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
25QT_BEGIN_NAMESPACE
26
27using namespace Qt::StringLiterals;
28
29void 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
44int 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)
80QString 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
94void 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
110int 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 errorDevice.open(stderr, ioFlags: QIODevice::WriteOnly|QIODevice::Text);
307
308 if (library.verbose())
309 errorDevice.write(data: "Qt resource compiler\n");
310
311 library.setInputFiles(filenamesIn);
312
313 if (!library.readFiles(listMode: list || map, errorDevice))
314 return 1;
315
316 QFile out;
317
318 // open output
319 QIODevice::OpenMode mode = QIODevice::NotOpen;
320 switch (library.format()) {
321 case RCCResourceLibrary::C_Code:
322 case RCCResourceLibrary::Pass1:
323 case RCCResourceLibrary::Python_Code:
324 mode = QIODevice::WriteOnly | QIODevice::Text;
325 break;
326 case RCCResourceLibrary::Pass2:
327 case RCCResourceLibrary::Binary:
328 mode = QIODevice::WriteOnly;
329 break;
330 }
331
332
333 if (outFilename.isEmpty() || outFilename == "-"_L1) {
334#ifdef Q_OS_WIN
335 // Make sure fwrite to stdout doesn't do LF->CRLF
336 if (library.format() == RCCResourceLibrary::Binary)
337 _setmode(_fileno(stdout), _O_BINARY);
338 // Make sure QIODevice does not do LF->CRLF,
339 // otherwise we'll end up in CRCRLF instead of
340 // CRLF.
341 mode &= ~QIODevice::Text;
342#endif // Q_OS_WIN
343 // using this overload close() only flushes.
344 out.open(stdout, ioFlags: mode);
345 } else {
346 out.setFileName(outFilename);
347 if (!out.open(flags: mode)) {
348 const QString msg = QString::fromLatin1(ba: "Unable to open %1 for writing: %2\n")
349 .arg(args&: outFilename, args: out.errorString());
350 errorDevice.write(data: msg.toUtf8());
351 return 1;
352 }
353 }
354
355 // do the task
356 if (list) {
357 const QStringList data = library.dataFiles();
358 for (int i = 0; i < data.size(); ++i) {
359 out.write(qPrintable(QDir::cleanPath(data.at(i))));
360 out.write(data: "\n");
361 }
362 return 0;
363 }
364
365 if (map) {
366 const RCCResourceLibrary::ResourceDataFileMap data = library.resourceDataFileMap();
367 for (auto it = data.begin(), end = data.end(); it != end; ++it) {
368 out.write(qPrintable(it.key()));
369 out.write(data: "\t");
370 out.write(qPrintable(QDir::cleanPath(it.value())));
371 out.write(data: "\n");
372 }
373 return 0;
374 }
375
376 // Write depfile
377 if (!depFilename.isEmpty()) {
378 QFile depout;
379 depout.setFileName(depFilename);
380
381 if (outFilename.isEmpty() || outFilename == "-"_L1) {
382 const QString msg = QString::fromUtf8(utf8: "Unable to write depfile when outputting to stdout!\n");
383 errorDevice.write(data: msg.toUtf8());
384 return 1;
385 }
386
387 if (!depout.open(flags: QIODevice::WriteOnly | QIODevice::Text)) {
388 const QString msg = QString::fromUtf8(utf8: "Unable to open depfile %1 for writing: %2\n")
389 .arg(args: depout.fileName(), args: depout.errorString());
390 errorDevice.write(data: msg.toUtf8());
391 return 1;
392 }
393
394 writeDepFile(iodev&: depout, depsList: library.dataFiles(), targetName: outFilename);
395 depout.close();
396 }
397
398 QFile temp;
399 if (!tempFilename.isEmpty()) {
400 temp.setFileName(tempFilename);
401 if (!temp.open(flags: QIODevice::ReadOnly)) {
402 const QString msg = QString::fromUtf8(utf8: "Unable to open temporary file %1 for reading: %2\n")
403 .arg(args&: tempFilename, args: out.errorString());
404 errorDevice.write(data: msg.toUtf8());
405 return 1;
406 }
407 }
408 bool success = library.output(outDevice&: out, tempDevice&: temp, errorDevice);
409 if (!success) {
410 // erase the output file if we failed
411 out.remove();
412 return 1;
413 }
414 return 0;
415}
416
417QT_END_NAMESPACE
418
419int main(int argc, char *argv[])
420{
421 // rcc uses a QHash to store files in the resource system.
422 // we must force a certain hash order when testing or tst_rcc will fail, see QTBUG-25078
423 // similar requirements exist for reproducibly builds.
424 QHashSeed::setDeterministicGlobalSeed();
425
426 return QT_PREPEND_NAMESPACE(runRcc)(argc, argv);
427}
428

source code of qtbase/src/tools/rcc/main.cpp