| 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 | |