| 1 | /**************************************************************************** |
| 2 | ** |
| 3 | ** Copyright (C) 2018 The Qt Company Ltd. |
| 4 | ** Copyright (C) 2018 Intel Corporation. |
| 5 | ** Contact: https://www.qt.io/licensing/ |
| 6 | ** |
| 7 | ** This file is part of the tools applications of the Qt Toolkit. |
| 8 | ** |
| 9 | ** $QT_BEGIN_LICENSE:GPL-EXCEPT$ |
| 10 | ** Commercial License Usage |
| 11 | ** Licensees holding valid commercial Qt licenses may use this file in |
| 12 | ** accordance with the commercial license agreement provided with the |
| 13 | ** Software or, alternatively, in accordance with the terms contained in |
| 14 | ** a written agreement between you and The Qt Company. For licensing terms |
| 15 | ** and conditions see https://www.qt.io/terms-conditions. For further |
| 16 | ** information use the contact form at https://www.qt.io/contact-us. |
| 17 | ** |
| 18 | ** GNU General Public License Usage |
| 19 | ** Alternatively, this file may be used under the terms of the GNU |
| 20 | ** General Public License version 3 as published by the Free Software |
| 21 | ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT |
| 22 | ** included in the packaging of this file. Please review the following |
| 23 | ** information to ensure the GNU General Public License requirements will |
| 24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. |
| 25 | ** |
| 26 | ** $QT_END_LICENSE$ |
| 27 | ** |
| 28 | ****************************************************************************/ |
| 29 | |
| 30 | #include <rcc.h> |
| 31 | |
| 32 | #include <qdebug.h> |
| 33 | #include <qdir.h> |
| 34 | #include <qfile.h> |
| 35 | #include <qfileinfo.h> |
| 36 | #include <qhashfunctions.h> |
| 37 | #include <qtextstream.h> |
| 38 | #include <qatomic.h> |
| 39 | #include <qglobal.h> |
| 40 | #include <qcoreapplication.h> |
| 41 | #include <qcommandlineoption.h> |
| 42 | #include <qcommandlineparser.h> |
| 43 | |
| 44 | #ifdef Q_OS_WIN |
| 45 | # include <fcntl.h> |
| 46 | # include <io.h> |
| 47 | # include <stdio.h> |
| 48 | #endif // Q_OS_WIN |
| 49 | |
| 50 | QT_BEGIN_NAMESPACE |
| 51 | |
| 52 | void dumpRecursive(const QDir &dir, QTextStream &out) |
| 53 | { |
| 54 | const QFileInfoList entries = dir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot |
| 55 | | QDir::NoSymLinks); |
| 56 | for (const QFileInfo &entry : entries) { |
| 57 | if (entry.isDir()) { |
| 58 | dumpRecursive(entry.filePath(), out); |
| 59 | } else { |
| 60 | out << QLatin1String("<file>" ) |
| 61 | << entry.filePath() |
| 62 | << QLatin1String("</file>\n" ); |
| 63 | } |
| 64 | } |
| 65 | } |
| 66 | |
| 67 | int createProject(const QString &outFileName) |
| 68 | { |
| 69 | QDir currentDir = QDir::current(); |
| 70 | QString currentDirName = currentDir.dirName(); |
| 71 | if (currentDirName.isEmpty()) |
| 72 | currentDirName = QLatin1String("root" ); |
| 73 | |
| 74 | QFile file; |
| 75 | bool isOk = false; |
| 76 | if (outFileName.isEmpty()) { |
| 77 | isOk = file.open(stdout, QFile::WriteOnly | QFile::Text); |
| 78 | } else { |
| 79 | file.setFileName(outFileName); |
| 80 | isOk = file.open(QFile::WriteOnly | QFile::Text); |
| 81 | } |
| 82 | if (!isOk) { |
| 83 | fprintf(stderr, "Unable to open %s: %s\n" , |
| 84 | outFileName.isEmpty() ? qPrintable(outFileName) : "standard output" , |
| 85 | qPrintable(file.errorString())); |
| 86 | return 1; |
| 87 | } |
| 88 | |
| 89 | QTextStream out(&file); |
| 90 | out << QLatin1String("<!DOCTYPE RCC><RCC version=\"1.0\">\n" |
| 91 | "<qresource>\n" ); |
| 92 | |
| 93 | // use "." as dir to get relative file pathes |
| 94 | dumpRecursive(QDir(QLatin1String("." )), out); |
| 95 | |
| 96 | out << QLatin1String("</qresource>\n" |
| 97 | "</RCC>\n" ); |
| 98 | |
| 99 | return 0; |
| 100 | } |
| 101 | |
| 102 | // Escapes a path for use in a Depfile (Makefile syntax) |
| 103 | QString makefileEscape(const QString &filepath) |
| 104 | { |
| 105 | // Always use forward slashes |
| 106 | QString result = QDir::cleanPath(filepath); |
| 107 | // Spaces are escaped with a backslash |
| 108 | result.replace(QLatin1Char(' '), QLatin1String("\\ " )); |
| 109 | // Pipes are escaped with a backslash |
| 110 | result.replace(QLatin1Char('|'), QLatin1String("\\|" )); |
| 111 | // Dollars are escaped with a dollar |
| 112 | result.replace(QLatin1Char('$'), QLatin1String("$$" )); |
| 113 | |
| 114 | return result; |
| 115 | } |
| 116 | |
| 117 | void writeDepFile(QIODevice &iodev, const QStringList &depsList, const QString &targetName) |
| 118 | { |
| 119 | QTextStream out(&iodev); |
| 120 | out << qPrintable(makefileEscape(targetName)); |
| 121 | out << QLatin1Char(':'); |
| 122 | |
| 123 | // Write depfile |
| 124 | for (int i = 0; i < depsList.size(); ++i) { |
| 125 | out << QLatin1Char(' '); |
| 126 | |
| 127 | out << qPrintable(makefileEscape(depsList.at(i))); |
| 128 | } |
| 129 | |
| 130 | out << QLatin1Char('\n'); |
| 131 | } |
| 132 | |
| 133 | int runRcc(int argc, char *argv[]) |
| 134 | { |
| 135 | QCoreApplication app(argc, argv); |
| 136 | QCoreApplication::setApplicationVersion(QStringLiteral(QT_VERSION_STR)); |
| 137 | |
| 138 | // Note that rcc isn't translated. |
| 139 | // If you use this code as an example for a translated app, make sure to translate the strings. |
| 140 | QCommandLineParser parser; |
| 141 | parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); |
| 142 | parser.setApplicationDescription(QLatin1String("Qt Resource Compiler version " QT_VERSION_STR)); |
| 143 | parser.addHelpOption(); |
| 144 | parser.addVersionOption(); |
| 145 | |
| 146 | QCommandLineOption outputOption(QStringList() << QStringLiteral("o" ) << QStringLiteral("output" )); |
| 147 | outputOption.setDescription(QStringLiteral("Write output to <file> rather than stdout." )); |
| 148 | outputOption.setValueName(QStringLiteral("file" )); |
| 149 | parser.addOption(outputOption); |
| 150 | |
| 151 | QCommandLineOption tempOption(QStringList() << QStringLiteral("t" ) << QStringLiteral("temp" )); |
| 152 | tempOption.setDescription(QStringLiteral("Use temporary <file> for big resources." )); |
| 153 | tempOption.setValueName(QStringLiteral("file" )); |
| 154 | parser.addOption(tempOption); |
| 155 | |
| 156 | QCommandLineOption nameOption(QStringLiteral("name" ), QStringLiteral("Create an external initialization function with <name>." ), QStringLiteral("name" )); |
| 157 | parser.addOption(nameOption); |
| 158 | |
| 159 | QCommandLineOption rootOption(QStringLiteral("root" ), QStringLiteral("Prefix resource access path with root path." ), QStringLiteral("path" )); |
| 160 | parser.addOption(rootOption); |
| 161 | |
| 162 | #if QT_CONFIG(zstd) && !defined(QT_NO_COMPRESS) |
| 163 | # define ALGOS "[zstd], zlib, none" |
| 164 | #elif QT_CONFIG(zstd) |
| 165 | # define ALGOS "[zstd], none" |
| 166 | #elif !defined(QT_NO_COMPRESS) |
| 167 | # define ALGOS "[zlib], none" |
| 168 | #else |
| 169 | # define ALGOS "[none]" |
| 170 | #endif |
| 171 | const QString &algoDescription = |
| 172 | QStringLiteral("Compress input files using algorithm <algo> (" ALGOS ")." ); |
| 173 | QCommandLineOption compressionAlgoOption(QStringLiteral("compress-algo" ), algoDescription, QStringLiteral("algo" )); |
| 174 | parser.addOption(compressionAlgoOption); |
| 175 | #undef ALGOS |
| 176 | |
| 177 | QCommandLineOption compressOption(QStringLiteral("compress" ), QStringLiteral("Compress input files by <level>." ), QStringLiteral("level" )); |
| 178 | parser.addOption(compressOption); |
| 179 | |
| 180 | QCommandLineOption nocompressOption(QStringLiteral("no-compress" ), QStringLiteral("Disable all compression. Same as --compress-algo=none." )); |
| 181 | parser.addOption(nocompressOption); |
| 182 | |
| 183 | QCommandLineOption noZstdOption(QStringLiteral("no-zstd" ), QStringLiteral("Disable usage of zstd compression." )); |
| 184 | parser.addOption(noZstdOption); |
| 185 | |
| 186 | QCommandLineOption thresholdOption(QStringLiteral("threshold" ), QStringLiteral("Threshold to consider compressing files." ), QStringLiteral("level" )); |
| 187 | parser.addOption(thresholdOption); |
| 188 | |
| 189 | QCommandLineOption binaryOption(QStringLiteral("binary" ), QStringLiteral("Output a binary file for use as a dynamic resource." )); |
| 190 | parser.addOption(binaryOption); |
| 191 | |
| 192 | QCommandLineOption generatorOption(QStringList{QStringLiteral("g" ), QStringLiteral("generator" )}); |
| 193 | generatorOption.setDescription(QStringLiteral("Select generator." )); |
| 194 | generatorOption.setValueName(QStringLiteral("cpp|python|python2" )); |
| 195 | parser.addOption(generatorOption); |
| 196 | |
| 197 | QCommandLineOption passOption(QStringLiteral("pass" ), QStringLiteral("Pass number for big resources" ), QStringLiteral("number" )); |
| 198 | parser.addOption(passOption); |
| 199 | |
| 200 | QCommandLineOption namespaceOption(QStringLiteral("namespace" ), QStringLiteral("Turn off namespace macros." )); |
| 201 | parser.addOption(namespaceOption); |
| 202 | |
| 203 | QCommandLineOption verboseOption(QStringLiteral("verbose" ), QStringLiteral("Enable verbose mode." )); |
| 204 | parser.addOption(verboseOption); |
| 205 | |
| 206 | QCommandLineOption listOption(QStringLiteral("list" ), QStringLiteral("Only list .qrc file entries, do not generate code." )); |
| 207 | parser.addOption(listOption); |
| 208 | |
| 209 | QCommandLineOption mapOption(QStringLiteral("list-mapping" ), |
| 210 | QStringLiteral("Only output a mapping of resource paths to file system paths defined in the .qrc file, do not generate code." )); |
| 211 | parser.addOption(mapOption); |
| 212 | |
| 213 | QCommandLineOption depFileOption(QStringList{QStringLiteral("d" ), QStringLiteral("depfile" )}, |
| 214 | QStringLiteral("Write a depfile with the .qrc dependencies to <file>." ), QStringLiteral("file" )); |
| 215 | parser.addOption(depFileOption); |
| 216 | |
| 217 | QCommandLineOption projectOption(QStringLiteral("project" ), QStringLiteral("Output a resource file containing all files from the current directory." )); |
| 218 | parser.addOption(projectOption); |
| 219 | |
| 220 | QCommandLineOption formatVersionOption(QStringLiteral("format-version" ), QStringLiteral("The RCC format version to write" ), QStringLiteral("number" )); |
| 221 | parser.addOption(formatVersionOption); |
| 222 | |
| 223 | parser.addPositionalArgument(QStringLiteral("inputs" ), QStringLiteral("Input files (*.qrc)." )); |
| 224 | |
| 225 | |
| 226 | //parse options |
| 227 | parser.process(app); |
| 228 | |
| 229 | QString errorMsg; |
| 230 | |
| 231 | quint8 formatVersion = 3; |
| 232 | if (parser.isSet(formatVersionOption)) { |
| 233 | bool ok = false; |
| 234 | formatVersion = parser.value(formatVersionOption).toUInt(&ok); |
| 235 | if (!ok) { |
| 236 | errorMsg = QLatin1String("Invalid format version specified" ); |
| 237 | } else if (formatVersion < 1 || formatVersion > 3) { |
| 238 | errorMsg = QLatin1String("Unsupported format version specified" ); |
| 239 | } |
| 240 | } |
| 241 | |
| 242 | RCCResourceLibrary library(formatVersion); |
| 243 | if (parser.isSet(nameOption)) |
| 244 | library.setInitName(parser.value(nameOption)); |
| 245 | if (parser.isSet(rootOption)) { |
| 246 | library.setResourceRoot(QDir::cleanPath(parser.value(rootOption))); |
| 247 | if (library.resourceRoot().isEmpty() |
| 248 | || library.resourceRoot().at(0) != QLatin1Char('/')) |
| 249 | errorMsg = QLatin1String("Root must start with a /" ); |
| 250 | } |
| 251 | |
| 252 | if (parser.isSet(compressionAlgoOption)) |
| 253 | library.setCompressionAlgorithm(RCCResourceLibrary::parseCompressionAlgorithm(parser.value(compressionAlgoOption), &errorMsg)); |
| 254 | if (formatVersion < 3 && library.compressionAlgorithm() == RCCResourceLibrary::CompressionAlgorithm::Zstd) |
| 255 | errorMsg = QLatin1String("Zstandard compression requires format version 3 or higher" ); |
| 256 | if (parser.isSet(nocompressOption)) |
| 257 | library.setCompressionAlgorithm(RCCResourceLibrary::CompressionAlgorithm::None); |
| 258 | if (parser.isSet(noZstdOption)) |
| 259 | library.setNoZstd(true); |
| 260 | if (parser.isSet(compressOption) && errorMsg.isEmpty()) { |
| 261 | int level = library.parseCompressionLevel(library.compressionAlgorithm(), parser.value(compressOption), &errorMsg); |
| 262 | library.setCompressLevel(level); |
| 263 | } |
| 264 | if (parser.isSet(thresholdOption)) |
| 265 | library.setCompressThreshold(parser.value(thresholdOption).toInt()); |
| 266 | if (parser.isSet(binaryOption)) |
| 267 | library.setFormat(RCCResourceLibrary::Binary); |
| 268 | if (parser.isSet(generatorOption)) { |
| 269 | auto value = parser.value(generatorOption); |
| 270 | if (value == QLatin1String("cpp" )) |
| 271 | library.setFormat(RCCResourceLibrary::C_Code); |
| 272 | else if (value == QLatin1String("python" )) |
| 273 | library.setFormat(RCCResourceLibrary::Python3_Code); |
| 274 | else if (value == QLatin1String("python2" )) |
| 275 | library.setFormat(RCCResourceLibrary::Python2_Code); |
| 276 | else |
| 277 | errorMsg = QLatin1String("Invalid generator: " ) + value; |
| 278 | } |
| 279 | |
| 280 | if (parser.isSet(passOption)) { |
| 281 | if (parser.value(passOption) == QLatin1String("1" )) |
| 282 | library.setFormat(RCCResourceLibrary::Pass1); |
| 283 | else if (parser.value(passOption) == QLatin1String("2" )) |
| 284 | library.setFormat(RCCResourceLibrary::Pass2); |
| 285 | else |
| 286 | errorMsg = QLatin1String("Pass number must be 1 or 2" ); |
| 287 | } |
| 288 | if (parser.isSet(namespaceOption)) |
| 289 | library.setUseNameSpace(!library.useNameSpace()); |
| 290 | if (parser.isSet(verboseOption)) |
| 291 | library.setVerbose(true); |
| 292 | |
| 293 | const bool list = parser.isSet(listOption); |
| 294 | const bool map = parser.isSet(mapOption); |
| 295 | const bool projectRequested = parser.isSet(projectOption); |
| 296 | const QStringList filenamesIn = parser.positionalArguments(); |
| 297 | |
| 298 | for (const QString &file : filenamesIn) { |
| 299 | if (file == QLatin1String("-" )) |
| 300 | continue; |
| 301 | else if (!QFile::exists(file)) { |
| 302 | qWarning("%s: File does not exist '%s'" , argv[0], qPrintable(file)); |
| 303 | return 1; |
| 304 | } |
| 305 | } |
| 306 | |
| 307 | QString outFilename = parser.value(outputOption); |
| 308 | QString tempFilename = parser.value(tempOption); |
| 309 | QString depFilename = parser.value(depFileOption); |
| 310 | |
| 311 | if (projectRequested) { |
| 312 | return createProject(outFilename); |
| 313 | } |
| 314 | |
| 315 | if (filenamesIn.isEmpty()) |
| 316 | errorMsg = QStringLiteral("No input files specified." ); |
| 317 | |
| 318 | if (!errorMsg.isEmpty()) { |
| 319 | fprintf(stderr, "%s: %s\n" , argv[0], qPrintable(errorMsg)); |
| 320 | parser.showHelp(1); |
| 321 | return 1; |
| 322 | } |
| 323 | QFile errorDevice; |
| 324 | errorDevice.open(stderr, QIODevice::WriteOnly|QIODevice::Text); |
| 325 | |
| 326 | if (library.verbose()) |
| 327 | errorDevice.write("Qt resource compiler\n" ); |
| 328 | |
| 329 | library.setInputFiles(filenamesIn); |
| 330 | |
| 331 | if (!library.readFiles(list || map, errorDevice)) |
| 332 | return 1; |
| 333 | |
| 334 | QFile out; |
| 335 | |
| 336 | // open output |
| 337 | QIODevice::OpenMode mode = QIODevice::NotOpen; |
| 338 | switch (library.format()) { |
| 339 | case RCCResourceLibrary::C_Code: |
| 340 | case RCCResourceLibrary::Pass1: |
| 341 | case RCCResourceLibrary::Python3_Code: |
| 342 | case RCCResourceLibrary::Python2_Code: |
| 343 | mode = QIODevice::WriteOnly | QIODevice::Text; |
| 344 | break; |
| 345 | case RCCResourceLibrary::Pass2: |
| 346 | case RCCResourceLibrary::Binary: |
| 347 | mode = QIODevice::WriteOnly; |
| 348 | break; |
| 349 | } |
| 350 | |
| 351 | |
| 352 | if (outFilename.isEmpty() || outFilename == QLatin1String("-" )) { |
| 353 | #ifdef Q_OS_WIN |
| 354 | // Make sure fwrite to stdout doesn't do LF->CRLF |
| 355 | if (library.format() == RCCResourceLibrary::Binary) |
| 356 | _setmode(_fileno(stdout), _O_BINARY); |
| 357 | // Make sure QIODevice does not do LF->CRLF, |
| 358 | // otherwise we'll end up in CRCRLF instead of |
| 359 | // CRLF. |
| 360 | mode &= ~QIODevice::Text; |
| 361 | #endif // Q_OS_WIN |
| 362 | // using this overload close() only flushes. |
| 363 | out.open(stdout, mode); |
| 364 | } else { |
| 365 | out.setFileName(outFilename); |
| 366 | if (!out.open(mode)) { |
| 367 | const QString msg = QString::fromLatin1("Unable to open %1 for writing: %2\n" ) |
| 368 | .arg(outFilename, out.errorString()); |
| 369 | errorDevice.write(msg.toUtf8()); |
| 370 | return 1; |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | // do the task |
| 375 | if (list) { |
| 376 | const QStringList data = library.dataFiles(); |
| 377 | for (int i = 0; i < data.size(); ++i) { |
| 378 | out.write(qPrintable(QDir::cleanPath(data.at(i)))); |
| 379 | out.write("\n" ); |
| 380 | } |
| 381 | return 0; |
| 382 | } |
| 383 | |
| 384 | if (map) { |
| 385 | const RCCResourceLibrary::ResourceDataFileMap data = library.resourceDataFileMap(); |
| 386 | for (auto it = data.begin(), end = data.end(); it != end; ++it) { |
| 387 | out.write(qPrintable(it.key())); |
| 388 | out.write("\t" ); |
| 389 | out.write(qPrintable(QDir::cleanPath(it.value()))); |
| 390 | out.write("\n" ); |
| 391 | } |
| 392 | return 0; |
| 393 | } |
| 394 | |
| 395 | // Write depfile |
| 396 | if (!depFilename.isEmpty()) { |
| 397 | QFile depout; |
| 398 | depout.setFileName(depFilename); |
| 399 | |
| 400 | if (outFilename.isEmpty() || outFilename == QLatin1String("-" )) { |
| 401 | const QString msg = QString::fromUtf8("Unable to write depfile when outputting to stdout!\n" ); |
| 402 | errorDevice.write(msg.toUtf8()); |
| 403 | return 1; |
| 404 | } |
| 405 | |
| 406 | if (!depout.open(QIODevice::WriteOnly | QIODevice::Text)) { |
| 407 | const QString msg = QString::fromUtf8("Unable to open depfile %1 for writing: %2\n" ) |
| 408 | .arg(depout.fileName(), depout.errorString()); |
| 409 | errorDevice.write(msg.toUtf8()); |
| 410 | return 1; |
| 411 | } |
| 412 | |
| 413 | writeDepFile(depout, library.dataFiles(), outFilename); |
| 414 | depout.close(); |
| 415 | } |
| 416 | |
| 417 | QFile temp; |
| 418 | if (!tempFilename.isEmpty()) { |
| 419 | temp.setFileName(tempFilename); |
| 420 | if (!temp.open(QIODevice::ReadOnly)) { |
| 421 | const QString msg = QString::fromUtf8("Unable to open temporary file %1 for reading: %2\n" ) |
| 422 | .arg(outFilename, out.errorString()); |
| 423 | errorDevice.write(msg.toUtf8()); |
| 424 | return 1; |
| 425 | } |
| 426 | } |
| 427 | bool success = library.output(out, temp, errorDevice); |
| 428 | if (!success) { |
| 429 | // erase the output file if we failed |
| 430 | out.remove(); |
| 431 | return 1; |
| 432 | } |
| 433 | return 0; |
| 434 | } |
| 435 | |
| 436 | QT_END_NAMESPACE |
| 437 | |
| 438 | int main(int argc, char *argv[]) |
| 439 | { |
| 440 | // rcc uses a QHash to store files in the resource system. |
| 441 | // we must force a certain hash order when testing or tst_rcc will fail, see QTBUG-25078 |
| 442 | // similar requirements exist for reproducibly builds. |
| 443 | qSetGlobalQHashSeed(0); |
| 444 | if (qGlobalQHashSeed() != 0) |
| 445 | qWarning("Cannot force QHash seed" ); |
| 446 | |
| 447 | return QT_PREPEND_NAMESPACE(runRcc)(argc, argv); |
| 448 | } |
| 449 | |