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 | |
28 | using namespace Qt::Literals::StringLiterals; |
29 | |
30 | static 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 | |
59 | int 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: dumpAotStatsOption) && !parser.isSet(option: moduleIdOption)) { |
144 | fprintf(stderr, format: "--dump-aot-stats set without setting --module-id" ); |
145 | return EXIT_FAILURE; |
146 | } |
147 | |
148 | const QStringList sources = parser.positionalArguments(); |
149 | if (sources.isEmpty()){ |
150 | parser.showHelp(); |
151 | } else if (sources.size() > 1 && (target != GenerateLoader && target != GenerateLoaderStandAlone)) { |
152 | fprintf(stderr, format: "%s\n" , qPrintable("Too many input files specified: '"_L1 + sources.join("' '"_L1 ) + u'\'')); |
153 | return EXIT_FAILURE; |
154 | } |
155 | |
156 | const QString inputFile = !sources.isEmpty() ? sources.first() : QString(); |
157 | if (outputFileName.isEmpty()) |
158 | outputFileName = inputFile + u'c'; |
159 | |
160 | if (parser.isSet(option: filterResourceFileOption)) |
161 | return qRelocateResourceFile(input: inputFile, output: outputFileName); |
162 | |
163 | if (target == GenerateLoader) { |
164 | QQmlJSResourceFileMapper mapper(sources); |
165 | |
166 | QQmlJSCompileError error; |
167 | if (!qQmlJSGenerateLoader( |
168 | compiledFiles: mapper.resourcePaths(filter: QQmlJSResourceFileMapper::allQmlJSFilter()), |
169 | outputFileName, resourceFileMappings: parser.values(option: resourceFileMappingOption), errorString: &error.message)) { |
170 | error.augment(contextErrorMessage: "Error generating loader stub: "_L1 ).print(); |
171 | return EXIT_FAILURE; |
172 | } |
173 | return EXIT_SUCCESS; |
174 | } |
175 | |
176 | if (target == GenerateLoaderStandAlone) { |
177 | QQmlJSCompileError error; |
178 | if (!qQmlJSGenerateLoader(compiledFiles: sources, outputFileName, |
179 | resourceFileMappings: parser.values(option: resourceNameOption), errorString: &error.message)) { |
180 | error.augment(contextErrorMessage: "Error generating loader stub: "_L1 ).print(); |
181 | return EXIT_FAILURE; |
182 | } |
183 | return EXIT_SUCCESS; |
184 | } |
185 | QString inputFileUrl = inputFile; |
186 | |
187 | QQmlJSSaveFunction saveFunction; |
188 | QQmlJSResourceFileMapper fileMapper(parser.values(option: resourceOption)); |
189 | QString inputResourcePath = parser.value(option: resourcePathOption); |
190 | |
191 | // If the user didn't specify the resource path corresponding to the file on disk being |
192 | // compiled, try to determine it from the resource file, if one was supplied. |
193 | if (inputResourcePath.isEmpty()) { |
194 | const QStringList resourcePaths = fileMapper.resourcePaths( |
195 | filter: QQmlJSResourceFileMapper::localFileFilter(file: inputFile)); |
196 | if (target == GenerateCpp && resourcePaths.isEmpty()) { |
197 | fprintf(stderr, format: "No resource path for file: %s\n" , qPrintable(inputFile)); |
198 | return EXIT_FAILURE; |
199 | } |
200 | |
201 | if (resourcePaths.size() == 1) { |
202 | inputResourcePath = resourcePaths.first(); |
203 | } else if (target == GenerateCpp) { |
204 | fprintf(stderr, format: "Multiple resource paths for file %s. " |
205 | "Use the --%s option to disambiguate:\n" , |
206 | qPrintable(inputFile), |
207 | qPrintable(resourcePathOption.names().first())); |
208 | for (const QString &resourcePath: resourcePaths) |
209 | fprintf(stderr, format: "\t%s\n" , qPrintable(resourcePath)); |
210 | return EXIT_FAILURE; |
211 | } |
212 | } |
213 | |
214 | if (target == GenerateCpp) { |
215 | inputFileUrl = "qrc://"_L1 + inputResourcePath; |
216 | saveFunction = [inputResourcePath, outputFileName]( |
217 | const QV4::CompiledData::SaveableUnitPointer &unit, |
218 | const QQmlJSAotFunctionMap &aotFunctions, |
219 | QString *errorString) { |
220 | return qSaveQmlJSUnitAsCpp(inputFileName: inputResourcePath, outputFileName, unit, aotFunctions, errorString); |
221 | }; |
222 | |
223 | } else { |
224 | saveFunction = [outputFileName](const QV4::CompiledData::SaveableUnitPointer &unit, |
225 | const QQmlJSAotFunctionMap &aotFunctions, |
226 | QString *errorString) { |
227 | Q_UNUSED(aotFunctions); |
228 | return unit.saveToDisk<char>( |
229 | writer: [&outputFileName, errorString](const char *data, quint32 size) { |
230 | return QV4::CompiledData::SaveableUnitPointer::writeDataToFile( |
231 | outputFileName, data, size, errorString); |
232 | }); |
233 | }; |
234 | } |
235 | |
236 | if (inputFile.endsWith(s: ".qml"_L1 )) { |
237 | QQmlJSCompileError error; |
238 | if (target != GenerateCpp || inputResourcePath.isEmpty() || parser.isSet(option: onlyBytecode)) { |
239 | if (!qCompileQmlFile(inputFileName: inputFile, saveFunction, aotCompiler: nullptr, error: &error, |
240 | /* storeSourceLocation */ false)) { |
241 | error.augment(contextErrorMessage: "Error compiling qml file: "_L1 ).print(); |
242 | return EXIT_FAILURE; |
243 | } |
244 | |
245 | if (parser.isSet(option: onlyBytecode)) { |
246 | QQmlJS::AotStats emptyStats; |
247 | emptyStats.saveToDisk(filepath: outputFileName + u".aotstats"_s ); |
248 | } |
249 | } else { |
250 | QStringList importPaths; |
251 | |
252 | if (parser.isSet(option: resourceOption)) { |
253 | importPaths.append(t: ":/qt-project.org/imports"_L1 ); |
254 | importPaths.append(t: ":/qt/qml"_L1 ); |
255 | }; |
256 | |
257 | if (parser.isSet(option: importPathOption)) |
258 | importPaths.append(other: parser.values(option: importPathOption)); |
259 | |
260 | if (!parser.isSet(option: bareOption)) |
261 | importPaths.append(t: QLibraryInfo::path(p: QLibraryInfo::QmlImportsPath)); |
262 | |
263 | QQmlJSImporter importer( |
264 | importPaths, parser.isSet(option: resourceOption) ? &fileMapper : nullptr); |
265 | QQmlJSLogger logger; |
266 | logger.setFilePath(inputFile); |
267 | |
268 | // Always trigger the qFatal() on "pragma Strict" violations. |
269 | logger.setCategoryLevel(id: qmlCompiler, level: QtWarningMsg); |
270 | logger.setCategoryIgnored(id: qmlCompiler, error: false); |
271 | logger.setCategoryFatal(id: qmlCompiler, error: true); |
272 | |
273 | if (!parser.isSet(option: verboseOption) && !parser.isSet(option: warningsAreErrorsOption)) |
274 | logger.setSilent(true); |
275 | |
276 | QQmlJSAotCompiler cppCodeGen( |
277 | &importer, u':' + inputResourcePath, |
278 | QQmlJSUtils::cleanPaths(paths: parser.values(option: importsOption)), &logger); |
279 | |
280 | if (parser.isSet(option: dumpAotStatsOption)) { |
281 | QQmlJS::QQmlJSAotCompilerStats::setRecordAotStats(true); |
282 | QQmlJS::QQmlJSAotCompilerStats::setModuleId(parser.value(option: moduleIdOption)); |
283 | QQmlJS::QQmlJSAotCompilerStats::registerFile(filepath: inputFile); |
284 | } |
285 | |
286 | if (parser.isSet(option: validateBasicBlocksOption)) |
287 | cppCodeGen.m_flags.setFlag(flag: QQmlJSAotCompiler::ValidateBasicBlocks); |
288 | |
289 | if (!qCompileQmlFile(inputFileName: inputFile, saveFunction, aotCompiler: &cppCodeGen, error: &error, |
290 | /* storeSourceLocation */ true)) { |
291 | error.augment(contextErrorMessage: "Error compiling qml file: "_L1 ).print(); |
292 | return EXIT_FAILURE; |
293 | } |
294 | |
295 | QList<QQmlJS::DiagnosticMessage> warnings = importer.takeGlobalWarnings(); |
296 | |
297 | if (!warnings.isEmpty()) { |
298 | logger.log(message: "Type warnings occurred while compiling file:"_L1 , |
299 | id: qmlImport, srcLocation: QQmlJS::SourceLocation()); |
300 | logger.processMessages(messages: warnings, id: qmlImport); |
301 | if (parser.isSet(option: warningsAreErrorsOption)) |
302 | return EXIT_FAILURE; |
303 | } |
304 | |
305 | if (parser.isSet(option: dumpAotStatsOption)) |
306 | QQmlJS::QQmlJSAotCompilerStats::instance()->saveToDisk(filepath: outputFileName + u".aotstats"_s ); |
307 | } |
308 | } else if (inputFile.endsWith(s: ".js"_L1 ) || inputFile.endsWith(s: ".mjs"_L1 )) { |
309 | QQmlJSCompileError error; |
310 | if (!qCompileJSFile(inputFileName: inputFile, inputFileUrl, saveFunction, error: &error)) { |
311 | error.augment(contextErrorMessage: "Error compiling js file: "_L1 ).print(); |
312 | return EXIT_FAILURE; |
313 | } |
314 | } else { |
315 | fprintf(stderr, format: "Ignoring %s input file as it is not QML source code - maybe remove from QML_FILES?\n" , qPrintable(inputFile)); |
316 | if (parser.isSet(option: warningsAreErrorsOption)) |
317 | return EXIT_FAILURE; |
318 | } |
319 | |
320 | return EXIT_SUCCESS; |
321 | } |
322 | |