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 | 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 | |
417 | QT_END_NAMESPACE |
418 | |
419 | int 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 | |