1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQml module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QCoreApplication>
30#include <QStringList>
31#include <QCommandLineParser>
32#include <QFile>
33#include <QFileInfo>
34#include <QDateTime>
35#include <QHashFunctions>
36#include <QSaveFile>
37#include <QScopedPointer>
38#include <QScopeGuard>
39
40#include <private/qqmlirbuilder_p.h>
41#include <private/qqmljsparser_p.h>
42#include <private/qqmljslexer_p.h>
43
44#include "resourcefilemapper.h"
45
46#include <algorithm>
47
48using namespace QQmlJS;
49
50int filterResourceFile(const QString &input, const QString &output);
51bool generateLoader(const QStringList &compiledFiles, const QString &output,
52 const QStringList &resourceFileMappings, QString *errorString);
53QString symbolNamespaceForPath(const QString &relativePath);
54
55QSet<QString> illegalNames;
56
57void setupIllegalNames()
58{
59 for (const char **g = QV4::Compiler::Codegen::s_globalNames; *g != nullptr; ++g)
60 illegalNames.insert(value: QString::fromLatin1(str: *g));
61}
62
63struct Error
64{
65 QString message;
66 void print();
67 Error augment(const QString &contextErrorMessage) const;
68 void appendDiagnostics(const QString &inputFileName, const QList<QQmlJS::DiagnosticMessage> &diagnostics);
69 void appendDiagnostic(const QString &inputFileName, const DiagnosticMessage &diagnostic);
70};
71
72void Error::print()
73{
74 fprintf(stderr, format: "%s\n", qPrintable(message));
75}
76
77Error Error::augment(const QString &contextErrorMessage) const
78{
79 Error augmented;
80 augmented.message = contextErrorMessage + message;
81 return augmented;
82}
83
84QString diagnosticErrorMessage(const QString &fileName, const QQmlJS::DiagnosticMessage &m)
85{
86 QString message;
87 message = fileName + QLatin1Char(':') + QString::number(m.loc.startLine) + QLatin1Char(':');
88 if (m.loc.startColumn > 0)
89 message += QString::number(m.loc.startColumn) + QLatin1Char(':');
90
91 if (m.isError())
92 message += QLatin1String(" error: ");
93 else
94 message += QLatin1String(" warning: ");
95 message += m.message;
96 return message;
97}
98
99void Error::appendDiagnostic(const QString &inputFileName, const DiagnosticMessage &diagnostic)
100{
101 if (!message.isEmpty())
102 message += QLatin1Char('\n');
103 message += diagnosticErrorMessage(fileName: inputFileName, m: diagnostic);
104}
105
106void Error::appendDiagnostics(const QString &inputFileName, const QList<DiagnosticMessage> &diagnostics)
107{
108 for (const QQmlJS::DiagnosticMessage &diagnostic: diagnostics)
109 appendDiagnostic(inputFileName, diagnostic);
110}
111
112// Ensure that ListElement objects keep all property assignments in their string form
113static void annotateListElements(QmlIR::Document *document)
114{
115 QStringList listElementNames;
116
117 for (const QV4::CompiledData::Import *import : qAsConst(t&: document->imports)) {
118 const QString uri = document->stringAt(index: import->uriIndex);
119 if (uri != QStringLiteral("QtQml.Models") && uri != QStringLiteral("QtQuick"))
120 continue;
121
122 QString listElementName = QStringLiteral("ListElement");
123 const QString qualifier = document->stringAt(index: import->qualifierIndex);
124 if (!qualifier.isEmpty()) {
125 listElementName.prepend(c: QLatin1Char('.'));
126 listElementName.prepend(s: qualifier);
127 }
128 listElementNames.append(t: listElementName);
129 }
130
131 if (listElementNames.isEmpty())
132 return;
133
134 for (QmlIR::Object *object : qAsConst(t&: document->objects)) {
135 if (!listElementNames.contains(str: document->stringAt(index: object->inheritedTypeNameIndex)))
136 continue;
137 for (QmlIR::Binding *binding = object->firstBinding(); binding; binding = binding->next) {
138 if (binding->type != QV4::CompiledData::Binding::Type_Script)
139 continue;
140 binding->stringIndex = document->registerString(str: object->bindingAsString(doc: document, scriptIndex: binding->value.compiledScriptIndex));
141 }
142 }
143}
144
145static bool checkArgumentsObjectUseInSignalHandlers(const QmlIR::Document &doc, Error *error)
146{
147 for (QmlIR::Object *object: qAsConst(t: doc.objects)) {
148 for (auto binding = object->bindingsBegin(); binding != object->bindingsEnd(); ++binding) {
149 if (binding->type != QV4::CompiledData::Binding::Type_Script)
150 continue;
151 const QString propName = doc.stringAt(index: binding->propertyNameIndex);
152 if (!propName.startsWith(s: QLatin1String("on"))
153 || propName.length() < 3
154 || !propName.at(i: 2).isUpper())
155 continue;
156 auto compiledFunction = doc.jsModule.functions.value(i: object->runtimeFunctionIndices.at(index: binding->value.compiledScriptIndex));
157 if (!compiledFunction)
158 continue;
159 if (compiledFunction->usesArgumentsObject == QV4::Compiler::Context::ArgumentsObjectUsed) {
160 error->message = QLatin1Char(':') + QString::number(compiledFunction->line) + QLatin1Char(':');
161 if (compiledFunction->column > 0)
162 error->message += QString::number(compiledFunction->column) + QLatin1Char(':');
163
164 error->message += QLatin1String(" error: The use of eval() or the use of the arguments object in signal handlers is\n"
165 "not supported when compiling qml files ahead of time. That is because it's ambiguous if \n"
166 "any signal parameter is called \"arguments\". Similarly the string passed to eval might use\n"
167 "\"arguments\". Unfortunately we cannot distinguish between it being a parameter or the\n"
168 "JavaScript arguments object at this point.\n"
169 "Consider renaming the parameter of the signal if applicable or moving the code into a\n"
170 "helper function.");
171 return false;
172 }
173 }
174 }
175 return true;
176}
177
178using SaveFunction = std::function<bool(const QV4::CompiledData::SaveableUnitPointer &, QString *)>;
179
180static bool compileQmlFile(const QString &inputFileName, SaveFunction saveFunction, Error *error)
181{
182 QmlIR::Document irDocument(/*debugMode*/false);
183
184 QString sourceCode;
185 {
186 QFile f(inputFileName);
187 if (!f.open(flags: QIODevice::ReadOnly)) {
188 error->message = QLatin1String("Error opening ") + inputFileName + QLatin1Char(':') + f.errorString();
189 return false;
190 }
191 sourceCode = QString::fromUtf8(str: f.readAll());
192 if (f.error() != QFileDevice::NoError) {
193 error->message = QLatin1String("Error reading from ") + inputFileName + QLatin1Char(':') + f.errorString();
194 return false;
195 }
196 }
197
198 {
199 QmlIR::IRBuilder irBuilder(illegalNames);
200 if (!irBuilder.generateFromQml(code: sourceCode, url: inputFileName, output: &irDocument)) {
201 error->appendDiagnostics(inputFileName, diagnostics: irBuilder.errors);
202 return false;
203 }
204 }
205
206 annotateListElements(document: &irDocument);
207
208 {
209 QmlIR::JSCodeGen v4CodeGen(&irDocument, illegalNames);
210 for (QmlIR::Object *object: qAsConst(t&: irDocument.objects)) {
211 if (object->functionsAndExpressions->count == 0)
212 continue;
213 QList<QmlIR::CompiledFunctionOrExpression> functionsToCompile;
214 for (QmlIR::CompiledFunctionOrExpression *foe = object->functionsAndExpressions->first; foe; foe = foe->next)
215 functionsToCompile << *foe;
216 const QVector<int> runtimeFunctionIndices = v4CodeGen.generateJSCodeForFunctionsAndBindings(functions: functionsToCompile);
217 if (v4CodeGen.hasError()) {
218 error->appendDiagnostic(inputFileName, diagnostic: v4CodeGen.error());
219 return false;
220 }
221
222 QQmlJS::MemoryPool *pool = irDocument.jsParserEngine.pool();
223 object->runtimeFunctionIndices.allocate(pool, vector: runtimeFunctionIndices);
224 }
225
226 if (!checkArgumentsObjectUseInSignalHandlers(doc: irDocument, error)) {
227 *error = error->augment(contextErrorMessage: inputFileName);
228 return false;
229 }
230
231 QmlIR::QmlUnitGenerator generator;
232 irDocument.javaScriptCompilationUnit = v4CodeGen.generateCompilationUnit(/*generate unit*/generateUnitData: false);
233 generator.generate(output&: irDocument);
234
235 const quint32 saveFlags
236 = QV4::CompiledData::Unit::StaticData
237 | QV4::CompiledData::Unit::PendingTypeCompilation;
238 QV4::CompiledData::SaveableUnitPointer saveable(irDocument.javaScriptCompilationUnit.data,
239 saveFlags);
240 if (!saveFunction(saveable, &error->message))
241 return false;
242 }
243 return true;
244}
245
246static bool compileJSFile(const QString &inputFileName, const QString &inputFileUrl, SaveFunction saveFunction, Error *error)
247{
248 QV4::CompiledData::CompilationUnit unit;
249
250 QString sourceCode;
251 {
252 QFile f(inputFileName);
253 if (!f.open(flags: QIODevice::ReadOnly)) {
254 error->message = QLatin1String("Error opening ") + inputFileName + QLatin1Char(':') + f.errorString();
255 return false;
256 }
257 sourceCode = QString::fromUtf8(str: f.readAll());
258 if (f.error() != QFileDevice::NoError) {
259 error->message = QLatin1String("Error reading from ") + inputFileName + QLatin1Char(':') + f.errorString();
260 return false;
261 }
262 }
263
264 const bool isModule = inputFileName.endsWith(s: QLatin1String(".mjs"));
265 if (isModule) {
266 QList<QQmlJS::DiagnosticMessage> diagnostics;
267 // Precompiled files are relocatable and the final location will be set when loading.
268 QString url;
269 unit = QV4::Compiler::Codegen::compileModule(/*debugMode*/false, url, sourceCode,
270 sourceTimeStamp: QDateTime(), diagnostics: &diagnostics);
271 error->appendDiagnostics(inputFileName, diagnostics);
272 if (!unit.unitData())
273 return false;
274 } else {
275 QmlIR::Document irDocument(/*debugMode*/false);
276
277 QQmlJS::Engine *engine = &irDocument.jsParserEngine;
278 QmlIR::ScriptDirectivesCollector directivesCollector(&irDocument);
279 QQmlJS::Directives *oldDirs = engine->directives();
280 engine->setDirectives(&directivesCollector);
281 auto directivesGuard = qScopeGuard(f: [engine, oldDirs]{
282 engine->setDirectives(oldDirs);
283 });
284
285 QQmlJS::AST::Program *program = nullptr;
286
287 {
288 QQmlJS::Lexer lexer(engine);
289 lexer.setCode(code: sourceCode, /*line*/lineno: 1, /*parseAsBinding*/qmlMode: false);
290 QQmlJS::Parser parser(engine);
291
292 bool parsed = parser.parseProgram();
293
294 error->appendDiagnostics(inputFileName, diagnostics: parser.diagnosticMessages());
295
296 if (!parsed)
297 return false;
298
299 program = QQmlJS::AST::cast<QQmlJS::AST::Program*>(parser.rootNode());
300 if (!program) {
301 lexer.setCode(QStringLiteral("undefined;"), lineno: 1, qmlMode: false);
302 parsed = parser.parseProgram();
303 Q_ASSERT(parsed);
304 program = QQmlJS::AST::cast<QQmlJS::AST::Program*>(parser.rootNode());
305 Q_ASSERT(program);
306 }
307 }
308
309 {
310 QmlIR::JSCodeGen v4CodeGen(&irDocument, illegalNames);
311 v4CodeGen.generateFromProgram(fileName: inputFileName, finalUrl: inputFileUrl, sourceCode, ast: program,
312 module: &irDocument.jsModule, contextType: QV4::Compiler::ContextType::ScriptImportedByQML);
313 if (v4CodeGen.hasError()) {
314 error->appendDiagnostic(inputFileName, diagnostic: v4CodeGen.error());
315 return false;
316 }
317
318 // Precompiled files are relocatable and the final location will be set when loading.
319 irDocument.jsModule.fileName.clear();
320 irDocument.jsModule.finalUrl.clear();
321
322 irDocument.javaScriptCompilationUnit = v4CodeGen.generateCompilationUnit(/*generate unit*/generateUnitData: false);
323 QmlIR::QmlUnitGenerator generator;
324 generator.generate(output&: irDocument);
325 unit = std::move(irDocument.javaScriptCompilationUnit);
326 }
327 }
328
329 return saveFunction(QV4::CompiledData::SaveableUnitPointer(unit.data), &error->message);
330}
331
332static bool saveUnitAsCpp(const QString &inputFileName, const QString &outputFileName,
333 const QV4::CompiledData::SaveableUnitPointer &unit,
334 QString *errorString)
335{
336#if QT_CONFIG(temporaryfile)
337 QSaveFile f(outputFileName);
338#else
339 QFile f(outputFileName);
340#endif
341 if (!f.open(flags: QIODevice::WriteOnly | QIODevice::Truncate)) {
342 *errorString = f.errorString();
343 return false;
344 }
345
346 auto writeStr = [&f, errorString](const QByteArray &data) {
347 if (f.write(data) != data.size()) {
348 *errorString = f.errorString();
349 return false;
350 }
351 return true;
352 };
353
354 if (!writeStr("// "))
355 return false;
356
357 if (!writeStr(inputFileName.toUtf8()))
358 return false;
359
360 if (!writeStr("\n"))
361 return false;
362
363 if (!writeStr(QByteArrayLiteral("namespace QmlCacheGeneratedCode {\nnamespace ")))
364 return false;
365
366 if (!writeStr(symbolNamespaceForPath(relativePath: inputFileName).toUtf8()))
367 return false;
368
369 if (!writeStr(QByteArrayLiteral(" {\nextern const unsigned char qmlData alignas(16) [] = {\n")))
370 return false;
371
372 unit.saveToDisk<uchar>(writer: [&writeStr](const uchar *begin, quint32 size) {
373 QByteArray hexifiedData;
374 {
375 QTextStream stream(&hexifiedData);
376 const uchar *end = begin + size;
377 stream << Qt::hex;
378 int col = 0;
379 for (const uchar *data = begin; data < end; ++data, ++col) {
380 if (data > begin)
381 stream << ',';
382 if (col % 8 == 0) {
383 stream << '\n';
384 col = 0;
385 }
386 stream << "0x" << *data;
387 }
388 stream << '\n';
389 }
390 return writeStr(hexifiedData);
391 });
392
393
394
395 if (!writeStr("};\n}\n}\n"))
396 return false;
397
398#if QT_CONFIG(temporaryfile)
399 if (!f.commit()) {
400 *errorString = f.errorString();
401 return false;
402 }
403#endif
404
405 return true;
406}
407
408int main(int argc, char **argv)
409{
410 // Produce reliably the same output for the same input by disabling QHash's random seeding.
411 qSetGlobalQHashSeed(newSeed: 0);
412
413 QCoreApplication app(argc, argv);
414 QCoreApplication::setApplicationName(QStringLiteral("qmlcachegen"));
415 QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR));
416
417 QCommandLineParser parser;
418 parser.addHelpOption();
419 parser.addVersionOption();
420
421 QCommandLineOption filterResourceFileOption(QStringLiteral("filter-resource-file"), QCoreApplication::translate(context: "main", key: "Filter out QML/JS files from a resource file that can be cached ahead of time instead"));
422 parser.addOption(commandLineOption: filterResourceFileOption);
423 QCommandLineOption resourceFileMappingOption(QStringLiteral("resource-file-mapping"), QCoreApplication::translate(context: "main", key: "Path from original resource file to new one"), QCoreApplication::translate(context: "main", key: "old-name:new-name"));
424 parser.addOption(commandLineOption: resourceFileMappingOption);
425 QCommandLineOption resourceOption(QStringLiteral("resource"), QCoreApplication::translate(context: "main", key: "Qt resource file that might later contain one of the compiled files"), QCoreApplication::translate(context: "main", key: "resource-file-name"));
426 parser.addOption(commandLineOption: resourceOption);
427 QCommandLineOption resourcePathOption(QStringLiteral("resource-path"), QCoreApplication::translate(context: "main", key: "Qt resource file path corresponding to the file being compiled"), QCoreApplication::translate(context: "main", key: "resource-path"));
428 parser.addOption(commandLineOption: resourcePathOption);
429
430 QCommandLineOption outputFileOption(QStringLiteral("o"), QCoreApplication::translate(context: "main", key: "Output file name"), QCoreApplication::translate(context: "main", key: "file name"));
431 parser.addOption(commandLineOption: outputFileOption);
432
433 parser.addPositionalArgument(QStringLiteral("[qml file]"),
434 QStringLiteral("QML source file to generate cache for."));
435
436 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
437
438 parser.process(app);
439
440 enum Output {
441 GenerateCpp,
442 GenerateCacheFile,
443 GenerateLoader
444 } target = GenerateCacheFile;
445
446 QString outputFileName;
447 if (parser.isSet(option: outputFileOption))
448 outputFileName = parser.value(option: outputFileOption);
449
450 if (outputFileName.endsWith(s: QLatin1String(".cpp"))) {
451 target = GenerateCpp;
452 if (outputFileName.endsWith(s: QLatin1String("qmlcache_loader.cpp")))
453 target = GenerateLoader;
454 }
455
456 const QStringList sources = parser.positionalArguments();
457 if (sources.isEmpty()){
458 parser.showHelp();
459 } else if (sources.count() > 1 && target != GenerateLoader) {
460 fprintf(stderr, format: "%s\n", qPrintable(QStringLiteral("Too many input files specified: '") + sources.join(QStringLiteral("' '")) + QLatin1Char('\'')));
461 return EXIT_FAILURE;
462 }
463
464 const QString inputFile = sources.first();
465 if (outputFileName.isEmpty())
466 outputFileName = inputFile + QLatin1Char('c');
467
468 if (parser.isSet(option: filterResourceFileOption)) {
469 return filterResourceFile(input: inputFile, output: outputFileName);
470 }
471
472 if (target == GenerateLoader) {
473 ResourceFileMapper mapper(sources);
474
475 Error error;
476 if (!generateLoader(compiledFiles: mapper.qmlCompilerFiles(), output: outputFileName,
477 resourceFileMappings: parser.values(option: resourceFileMappingOption), errorString: &error.message)) {
478 error.augment(contextErrorMessage: QLatin1String("Error generating loader stub: ")).print();
479 return EXIT_FAILURE;
480 }
481 return EXIT_SUCCESS;
482 }
483
484 QString inputFileUrl = inputFile;
485
486 SaveFunction saveFunction;
487 if (target == GenerateCpp) {
488 ResourceFileMapper fileMapper(parser.values(option: resourceOption));
489 QString inputResourcePath = parser.value(option: resourcePathOption);
490
491 if (!inputResourcePath.isEmpty() && !fileMapper.isEmpty()) {
492 fprintf(stderr, format: "--%s and --%s are mutually exclusive.\n",
493 qPrintable(resourcePathOption.names().first()),
494 qPrintable(resourceOption.names().first()));
495 return EXIT_FAILURE;
496 }
497
498 // If the user didn't specify the resource path corresponding to the file on disk being
499 // compiled, try to determine it from the resource file, if one was supplied.
500 if (inputResourcePath.isEmpty()) {
501 const QStringList resourcePaths = fileMapper.resourcePaths(fileName: inputFile);
502 if (resourcePaths.isEmpty()) {
503 fprintf(stderr, format: "No resource path for file: %s\n", qPrintable(inputFile));
504 return EXIT_FAILURE;
505 }
506
507 if (resourcePaths.size() != 1) {
508 fprintf(stderr, format: "Multiple resource paths for file %s. "
509 "Use the --%s option to disambiguate:\n",
510 qPrintable(inputFile),
511 qPrintable(resourcePathOption.names().first()));
512 for (const QString &resourcePath: resourcePaths)
513 fprintf(stderr, format: "\t%s\n", qPrintable(resourcePath));
514 return EXIT_FAILURE;
515 }
516
517 inputResourcePath = resourcePaths.first();
518 }
519
520 inputFileUrl = QStringLiteral("qrc://") + inputResourcePath;
521
522 saveFunction = [inputResourcePath, outputFileName](
523 const QV4::CompiledData::SaveableUnitPointer &unit,
524 QString *errorString) {
525 return saveUnitAsCpp(inputFileName: inputResourcePath, outputFileName, unit, errorString);
526 };
527
528 } else {
529 saveFunction = [outputFileName](const QV4::CompiledData::SaveableUnitPointer &unit,
530 QString *errorString) {
531 return unit.saveToDisk<char>(
532 writer: [&outputFileName, errorString](const char *data, quint32 size) {
533 return QV4::CompiledData::SaveableUnitPointer::writeDataToFile(
534 outputFileName, data, size, errorString);
535 });
536 };
537 }
538
539 setupIllegalNames();
540
541
542 if (inputFile.endsWith(s: QLatin1String(".qml"))) {
543 Error error;
544 if (!compileQmlFile(inputFileName: inputFile, saveFunction, error: &error)) {
545 error.augment(contextErrorMessage: QLatin1String("Error compiling qml file: ")).print();
546 return EXIT_FAILURE;
547 }
548 } else if (inputFile.endsWith(s: QLatin1String(".js")) || inputFile.endsWith(s: QLatin1String(".mjs"))) {
549 Error error;
550 if (!compileJSFile(inputFileName: inputFile, inputFileUrl, saveFunction, error: &error)) {
551 error.augment(contextErrorMessage: QLatin1String("Error compiling js file: ")).print();
552 return EXIT_FAILURE;
553 }
554 } else {
555 fprintf(stderr, format: "Ignoring %s input file as it is not QML source code - maybe remove from QML_FILES?\n", qPrintable(inputFile));
556 }
557
558 return EXIT_SUCCESS;
559}
560

source code of qtdeclarative/tools/qmlcachegen/qmlcachegen.cpp