1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
3
4#include <QCoreApplication>
5#include <QStringList>
6#include <QCommandLineParser>
7#include <QFile>
8#include <QFileInfo>
9#include <QDateTime>
10#include <QHashFunctions>
11#include <QSaveFile>
12#include <QScopedPointer>
13#include <QScopeGuard>
14#include <QLibraryInfo>
15#include <QLoggingCategory>
16
17#include <private/qqmlirbuilder_p.h>
18#include <private/qqmljscompiler_p.h>
19#include <private/qqmljslexer_p.h>
20#include <private/qqmljsloadergenerator_p.h>
21#include <private/qqmljsparser_p.h>
22#include <private/qqmljsresourcefilemapper_p.h>
23#include <private/qqmljsutils_p.h>
24#include <private/qresourcerelocater_p.h>
25
26#include <algorithm>
27
28using namespace Qt::Literals::StringLiterals;
29
30static bool argumentsFromCommandLineAndFile(QStringList& allArguments, const QStringList &arguments)
31{
32 allArguments.reserve(asize: arguments.size());
33 for (const QString &argument : arguments) {
34 // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it
35 if (argument.startsWith(c: u'@')) {
36 QString optionsFile = argument;
37 optionsFile.remove(i: 0, len: 1);
38 if (optionsFile.isEmpty()) {
39 fprintf(stderr, format: "The @ option requires an input file");
40 return false;
41 }
42 QFile f(optionsFile);
43 if (!f.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
44 fprintf(stderr, format: "Cannot open options file specified with @");
45 return false;
46 }
47 while (!f.atEnd()) {
48 QString line = QString::fromLocal8Bit(ba: f.readLine().trimmed());
49 if (!line.isEmpty())
50 allArguments << line;
51 }
52 } else {
53 allArguments << argument;
54 }
55 }
56 return true;
57}
58
59int main(int argc, char **argv)
60{
61 // Produce reliably the same output for the same input by disabling QHash's random seeding.
62 QHashSeed::setDeterministicGlobalSeed();
63
64 QCoreApplication app(argc, argv);
65 QCoreApplication::setApplicationName("qmlcachegen"_L1);
66 QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR));
67
68 QCommandLineParser parser;
69 parser.addHelpOption();
70 parser.addVersionOption();
71
72 QCommandLineOption bareOption("bare"_L1, QCoreApplication::translate(context: "main", key: "Do not include default import directories. This may be used to run qmlcachegen on a project using a different Qt version."));
73 parser.addOption(commandLineOption: bareOption);
74 QCommandLineOption filterResourceFileOption("filter-resource-file"_L1, QCoreApplication::translate(context: "main", key: "Filter out QML/JS files from a resource file that can be cached ahead of time instead"));
75 parser.addOption(commandLineOption: filterResourceFileOption);
76 QCommandLineOption resourceFileMappingOption("resource-file-mapping"_L1, QCoreApplication::translate(context: "main", key: "Path from original resource file to new one"), QCoreApplication::translate(context: "main", key: "old-name=new-name"));
77 parser.addOption(commandLineOption: resourceFileMappingOption);
78 QCommandLineOption resourceOption("resource"_L1, 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"));
79 parser.addOption(commandLineOption: resourceOption);
80 QCommandLineOption resourcePathOption("resource-path"_L1, QCoreApplication::translate(context: "main", key: "Qt resource file path corresponding to the file being compiled"), QCoreApplication::translate(context: "main", key: "resource-path"));
81 parser.addOption(commandLineOption: resourcePathOption);
82 QCommandLineOption resourceNameOption("resource-name"_L1, QCoreApplication::translate(context: "main", key: "Required to generate qmlcache_loader without qrc files. This is the name of the Qt resource the input files belong to."), QCoreApplication::translate(context: "main", key: "compiled-file-list"));
83 parser.addOption(commandLineOption: resourceNameOption);
84 QCommandLineOption directCallsOption("direct-calls"_L1, QCoreApplication::translate(context: "main", key: "This option is ignored."));
85 directCallsOption.setFlags(QCommandLineOption::HiddenFromHelp);
86 parser.addOption(commandLineOption: directCallsOption);
87 QCommandLineOption staticOption("static"_L1, QCoreApplication::translate(context: "main", key: "This option is ignored."));
88 staticOption.setFlags(QCommandLineOption::HiddenFromHelp);
89 parser.addOption(commandLineOption: staticOption);
90 QCommandLineOption importsOption("i"_L1, QCoreApplication::translate(context: "main", key: "Import extra qmldir"), QCoreApplication::translate(context: "main", key: "qmldir file"));
91 parser.addOption(commandLineOption: importsOption);
92 QCommandLineOption importPathOption("I"_L1, QCoreApplication::translate(context: "main", key: "Look for QML modules in specified directory"), QCoreApplication::translate(context: "main", key: "import directory"));
93 parser.addOption(commandLineOption: importPathOption);
94 QCommandLineOption onlyBytecode("only-bytecode"_L1, QCoreApplication::translate(context: "main", key: "Generate only byte code for bindings and functions, no C++ code"));
95 parser.addOption(commandLineOption: onlyBytecode);
96 QCommandLineOption verboseOption("verbose"_L1, QCoreApplication::translate(context: "main", key: "Output compile warnings"));
97 parser.addOption(commandLineOption: verboseOption);
98 QCommandLineOption warningsAreErrorsOption("warnings-are-errors"_L1, QCoreApplication::translate(context: "main", key: "Treat warnings as errors"));
99 parser.addOption(commandLineOption: warningsAreErrorsOption);
100
101 QCommandLineOption validateBasicBlocksOption("validate-basic-blocks"_L1, QCoreApplication::translate(context: "main", key: "Performs checks on the basic blocks of a function compiled ahead of time to validate its structure and coherence"));
102 parser.addOption(commandLineOption: validateBasicBlocksOption);
103
104 QCommandLineOption dumpAotStatsOption("dump-aot-stats"_L1, QCoreApplication::translate(context: "main", key: "Dumps statistics about ahead-of-time compilation of bindings and functions"));
105 parser.addOption(commandLineOption: dumpAotStatsOption);
106 QCommandLineOption moduleIdOption("module-id"_L1, QCoreApplication::translate(context: "main", key: "Identifies the module of the qml file being compiled for aot stats"), QCoreApplication::translate(context: "main", key: "id"));
107 parser.addOption(commandLineOption: moduleIdOption);
108
109 QCommandLineOption outputFileOption("o"_L1, QCoreApplication::translate(context: "main", key: "Output file name"), QCoreApplication::translate(context: "main", key: "file name"));
110 parser.addOption(commandLineOption: outputFileOption);
111
112 parser.addPositionalArgument(name: "[qml file]"_L1, description: "QML source file to generate cache for."_L1);
113
114 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
115
116
117 QStringList arguments;
118 if (!argumentsFromCommandLineAndFile(allArguments&: arguments, arguments: app.arguments()))
119 return EXIT_FAILURE;
120
121 parser.process(arguments);
122
123 enum Output {
124 GenerateCpp,
125 GenerateCacheFile,
126 GenerateLoader,
127 GenerateLoaderStandAlone,
128 } target = GenerateCacheFile;
129
130 QString outputFileName;
131 if (parser.isSet(option: outputFileOption))
132 outputFileName = parser.value(option: outputFileOption);
133
134 if (outputFileName.endsWith(s: ".cpp"_L1)) {
135 target = GenerateCpp;
136 if (outputFileName.endsWith(s: "qmlcache_loader.cpp"_L1))
137 target = GenerateLoader;
138 }
139
140 if (target == GenerateLoader && parser.isSet(option: resourceNameOption))
141 target = GenerateLoaderStandAlone;
142
143 if (parser.isSet(option: onlyBytecode)) {
144 const std::array<QCommandLineOption *, 3> compilerOnlyOptions{
145 &directCallsOption, &staticOption, &validateBasicBlocksOption
146 };
147
148 for (auto *compilerOnlyOption : compilerOnlyOptions) {
149 if (parser.isSet(option: *compilerOnlyOption)) {
150 std::string name = compilerOnlyOption->names().first().toStdString();
151 fprintf(stderr, format: "Passing mutually exclusive options \"only-bytecode\" and \"%s\".\n"
152 "Remove --only-bytecode to be able to use compiler options like --%s\n",
153 name.c_str(), name.c_str());
154 return EXIT_FAILURE;
155 }
156 }
157 }
158
159 if (parser.isSet(option: dumpAotStatsOption) && !parser.isSet(option: moduleIdOption)) {
160 fprintf(stderr, format: "--dump-aot-stats set without setting --module-id");
161 return EXIT_FAILURE;
162 }
163
164 const QStringList sources = parser.positionalArguments();
165 if (sources.isEmpty()){
166 parser.showHelp();
167 } else if (sources.size() > 1 && (target != GenerateLoader && target != GenerateLoaderStandAlone)) {
168 fprintf(stderr, format: "%s\n", qPrintable("Too many input files specified: '"_L1 + sources.join("' '"_L1) + u'\''));
169 return EXIT_FAILURE;
170 }
171
172 const QString inputFile = !sources.isEmpty() ? sources.first() : QString();
173 if (outputFileName.isEmpty())
174 outputFileName = inputFile + u'c';
175
176 if (parser.isSet(option: filterResourceFileOption))
177 return qRelocateResourceFile(input: inputFile, output: outputFileName);
178
179 if (target == GenerateLoader) {
180 QQmlJSResourceFileMapper mapper(sources);
181
182 QQmlJSCompileError error;
183 if (!qQmlJSGenerateLoader(
184 compiledFiles: mapper.resourcePaths(filter: QQmlJSResourceFileMapper::allQmlJSFilter()),
185 outputFileName, resourceFileMappings: parser.values(option: resourceFileMappingOption), errorString: &error.message)) {
186 error.augment(contextErrorMessage: "Error generating loader stub: "_L1).print();
187 return EXIT_FAILURE;
188 }
189 return EXIT_SUCCESS;
190 }
191
192 if (target == GenerateLoaderStandAlone) {
193 QQmlJSCompileError error;
194 if (!qQmlJSGenerateLoader(compiledFiles: sources, outputFileName,
195 resourceFileMappings: parser.values(option: resourceNameOption), errorString: &error.message)) {
196 error.augment(contextErrorMessage: "Error generating loader stub: "_L1).print();
197 return EXIT_FAILURE;
198 }
199 return EXIT_SUCCESS;
200 }
201 QString inputFileUrl = inputFile;
202
203 QQmlJSSaveFunction saveFunction;
204 QQmlJSResourceFileMapper fileMapper(parser.values(option: resourceOption));
205 QString inputResourcePath = parser.value(option: resourcePathOption);
206
207 // If the user didn't specify the resource path corresponding to the file on disk being
208 // compiled, try to determine it from the resource file, if one was supplied.
209 if (inputResourcePath.isEmpty()) {
210 const QStringList resourcePaths = fileMapper.resourcePaths(
211 filter: QQmlJSResourceFileMapper::localFileFilter(file: inputFile));
212 if (target == GenerateCpp && resourcePaths.isEmpty()) {
213 fprintf(stderr, format: "No resource path for file: %s\n", qPrintable(inputFile));
214 return EXIT_FAILURE;
215 }
216
217 if (resourcePaths.size() == 1) {
218 inputResourcePath = resourcePaths.first();
219 } else if (target == GenerateCpp) {
220 fprintf(stderr, format: "Multiple resource paths for file %s. "
221 "Use the --%s option to disambiguate:\n",
222 qPrintable(inputFile),
223 qPrintable(resourcePathOption.names().first()));
224 for (const QString &resourcePath: resourcePaths)
225 fprintf(stderr, format: "\t%s\n", qPrintable(resourcePath));
226 return EXIT_FAILURE;
227 }
228 }
229
230 if (target == GenerateCpp) {
231 inputFileUrl = "qrc://"_L1 + inputResourcePath;
232 saveFunction = [inputResourcePath, outputFileName](
233 const QV4::CompiledData::SaveableUnitPointer &unit,
234 const QQmlJSAotFunctionMap &aotFunctions,
235 QString *errorString) {
236 return qSaveQmlJSUnitAsCpp(inputFileName: inputResourcePath, outputFileName, unit, aotFunctions, errorString);
237 };
238
239 } else {
240 saveFunction = [outputFileName](const QV4::CompiledData::SaveableUnitPointer &unit,
241 const QQmlJSAotFunctionMap &aotFunctions,
242 QString *errorString) {
243 Q_UNUSED(aotFunctions);
244 return unit.saveToDisk<char>(
245 writer: [&outputFileName, errorString](const char *data, quint32 size) {
246 return QV4::CompiledData::SaveableUnitPointer::writeDataToFile(
247 outputFileName, data, size, errorString);
248 });
249 };
250 }
251
252 if (inputFile.endsWith(s: ".qml"_L1)) {
253 QQmlJSCompileError error;
254 if (target != GenerateCpp || inputResourcePath.isEmpty() || parser.isSet(option: onlyBytecode)) {
255 if (!qCompileQmlFile(inputFileName: inputFile, saveFunction, aotCompiler: nullptr, error: &error,
256 /* storeSourceLocation */ false)) {
257 error.augment(contextErrorMessage: "Error compiling qml file: "_L1).print();
258 return EXIT_FAILURE;
259 }
260
261 if (parser.isSet(option: onlyBytecode)) {
262 QQmlJS::AotStats emptyStats;
263 emptyStats.saveToDisk(filepath: outputFileName + u".aotstats"_s);
264 }
265 } else {
266 QStringList importPaths;
267
268 if (parser.isSet(option: resourceOption)) {
269 importPaths.append(t: ":/qt-project.org/imports"_L1);
270 importPaths.append(t: ":/qt/qml"_L1);
271 };
272
273 if (parser.isSet(option: importPathOption))
274 importPaths.append(other: parser.values(option: importPathOption));
275
276 if (!parser.isSet(option: bareOption))
277 importPaths.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath));
278
279 QQmlJSImporter importer(
280 importPaths, parser.isSet(option: resourceOption) ? &fileMapper : nullptr);
281 QQmlJSLogger logger;
282 logger.setFilePath(inputFile);
283
284 // Always trigger the qFatal() on "pragma Strict" violations.
285 logger.setCategoryLevel(id: qmlCompiler, level: QtWarningMsg);
286 logger.setCategoryIgnored(id: qmlCompiler, error: false);
287 logger.setCategoryFatal(id: qmlCompiler, error: true);
288
289 if (!parser.isSet(option: verboseOption) && !parser.isSet(option: warningsAreErrorsOption))
290 logger.setSilent(true);
291
292 QQmlJSAotCompiler cppCodeGen(
293 &importer, u':' + inputResourcePath,
294 QQmlJSUtils::cleanPaths(paths: parser.values(option: importsOption)), &logger);
295
296 if (parser.isSet(option: dumpAotStatsOption)) {
297 QQmlJS::QQmlJSAotCompilerStats::setRecordAotStats(true);
298 QQmlJS::QQmlJSAotCompilerStats::setModuleId(parser.value(option: moduleIdOption));
299 QQmlJS::QQmlJSAotCompilerStats::registerFile(filepath: inputFile);
300 }
301
302 if (parser.isSet(option: validateBasicBlocksOption))
303 cppCodeGen.m_flags.setFlag(flag: QQmlJSAotCompiler::ValidateBasicBlocks);
304
305 if (!qCompileQmlFile(inputFileName: inputFile, saveFunction, aotCompiler: &cppCodeGen, error: &error,
306 /* storeSourceLocation */ true)) {
307 error.augment(contextErrorMessage: "Error compiling qml file: "_L1).print();
308 return EXIT_FAILURE;
309 }
310
311 QList<QQmlJS::DiagnosticMessage> warnings = importer.takeGlobalWarnings();
312
313 if (!warnings.isEmpty()) {
314 logger.log(message: "Type warnings occurred while compiling file:"_L1,
315 id: qmlImport, srcLocation: QQmlJS::SourceLocation());
316 logger.processMessages(messages: warnings, id: qmlImport);
317 if (parser.isSet(option: warningsAreErrorsOption))
318 return EXIT_FAILURE;
319 }
320
321 if (parser.isSet(option: dumpAotStatsOption))
322 QQmlJS::QQmlJSAotCompilerStats::instance()->saveToDisk(filepath: outputFileName + u".aotstats"_s);
323 }
324 } else if (inputFile.endsWith(s: ".js"_L1) || inputFile.endsWith(s: ".mjs"_L1)) {
325 QQmlJSCompileError error;
326 if (!qCompileJSFile(inputFileName: inputFile, inputFileUrl, saveFunction, error: &error)) {
327 error.augment(contextErrorMessage: "Error compiling js file: "_L1).print();
328 return EXIT_FAILURE;
329 }
330 } else {
331 fprintf(stderr, format: "Ignoring %s input file as it is not QML source code - maybe remove from QML_FILES?\n", qPrintable(inputFile));
332 if (parser.isSet(option: warningsAreErrorsOption))
333 return EXIT_FAILURE;
334 }
335
336 return EXIT_SUCCESS;
337}
338

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