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