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
50QT_BEGIN_NAMESPACE
51
52void 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
67int 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)
103QString 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
117void 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
133int 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
436QT_END_NAMESPACE
437
438int 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