1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2019 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 "qmltypescreator.h" |
30 | |
31 | #include <QCoreApplication> |
32 | #include <QCommandLineParser> |
33 | #include <QtDebug> |
34 | #include <QJsonDocument> |
35 | #include <QJsonArray> |
36 | #include <QJsonValue> |
37 | #include <QJsonObject> |
38 | #include <QFile> |
39 | #include <QScopedPointer> |
40 | #include <QSaveFile> |
41 | #include <QQueue> |
42 | |
43 | #include <cstdlib> |
44 | |
45 | struct ScopedPointerFileCloser |
46 | { |
47 | static inline void cleanup(FILE *handle) { if (handle) fclose(stream: handle); } |
48 | }; |
49 | |
50 | enum RegistrationMode { |
51 | NoRegistration, |
52 | ObjectRegistration, |
53 | GadgetRegistration, |
54 | NamespaceRegistration |
55 | }; |
56 | |
57 | static RegistrationMode qmlTypeRegistrationMode(const QJsonObject &classDef) |
58 | { |
59 | const QJsonArray classInfos = classDef[QLatin1String("classInfos" )].toArray(); |
60 | for (const QJsonValue &info: classInfos) { |
61 | const QString name = info[QLatin1String("name" )].toString(); |
62 | if (name == QLatin1String("QML.Element" )) { |
63 | if (classDef[QLatin1String("object" )].toBool()) |
64 | return ObjectRegistration; |
65 | if (classDef[QLatin1String("gadget" )].toBool()) |
66 | return GadgetRegistration; |
67 | if (classDef[QLatin1String("namespace" )].toBool()) |
68 | return NamespaceRegistration; |
69 | qWarning() << "Not registering classInfo which is neither an object, " |
70 | "nor a gadget, nor a namespace:" |
71 | << name; |
72 | break; |
73 | } |
74 | } |
75 | return NoRegistration; |
76 | } |
77 | |
78 | static QVector<QJsonObject> foreignRelatedTypes(const QVector<QJsonObject> &types, |
79 | const QVector<QJsonObject> &foreignTypes) |
80 | { |
81 | const QLatin1String classInfosKey("classInfos" ); |
82 | const QLatin1String nameKey("name" ); |
83 | const QLatin1String qualifiedClassNameKey("qualifiedClassName" ); |
84 | const QLatin1String qmlNamePrefix("QML." ); |
85 | const QLatin1String qmlForeignName("QML.Foreign" ); |
86 | const QLatin1String qmlAttachedName("QML.Attached" ); |
87 | const QLatin1String valueKey("value" ); |
88 | const QLatin1String superClassesKey("superClasses" ); |
89 | const QLatin1String accessKey("access" ); |
90 | const QLatin1String publicAccess("public" ); |
91 | |
92 | QSet<QString> processedRelatedNames; |
93 | QQueue<QJsonObject> typeQueue; |
94 | typeQueue.append(t: types.toList()); |
95 | QVector<QJsonObject> relatedTypes; |
96 | |
97 | // First mark all classes registered from this module as already processed. |
98 | for (const QJsonObject &type : types) { |
99 | processedRelatedNames.insert(value: type.value(key: qualifiedClassNameKey).toString()); |
100 | const auto classInfos = type.value(key: classInfosKey).toArray(); |
101 | for (const QJsonValue &classInfo : classInfos) { |
102 | const QJsonObject obj = classInfo.toObject(); |
103 | if (obj.value(key: nameKey).toString() == qmlForeignName) { |
104 | processedRelatedNames.insert(value: obj.value(key: valueKey).toString()); |
105 | break; |
106 | } |
107 | } |
108 | } |
109 | |
110 | // Then mark all classes registered from other modules as already processed. |
111 | // We don't want to generate them again for this module. |
112 | for (const QJsonObject &foreignType : foreignTypes) { |
113 | const auto classInfos = foreignType.value(key: classInfosKey).toArray(); |
114 | bool seenQmlPrefix = false; |
115 | for (const QJsonValue &classInfo : classInfos) { |
116 | const QJsonObject obj = classInfo.toObject(); |
117 | const QString name = obj.value(key: nameKey).toString(); |
118 | if (!seenQmlPrefix && name.startsWith(s: qmlNamePrefix)) { |
119 | processedRelatedNames.insert(value: foreignType.value(key: qualifiedClassNameKey).toString()); |
120 | seenQmlPrefix = true; |
121 | } |
122 | if (name == qmlForeignName) { |
123 | processedRelatedNames.insert(value: obj.value(key: valueKey).toString()); |
124 | break; |
125 | } |
126 | } |
127 | } |
128 | |
129 | auto addType = [&](const QString &typeName) { |
130 | if (processedRelatedNames.contains(value: typeName)) |
131 | return; |
132 | processedRelatedNames.insert(value: typeName); |
133 | if (const QJsonObject *other = QmlTypesClassDescription::findType(types: foreignTypes, name: typeName)) { |
134 | relatedTypes.append(t: *other); |
135 | typeQueue.enqueue(t: *other); |
136 | } |
137 | }; |
138 | |
139 | // Then recursively iterate the super types and attached types, marking the |
140 | // ones we are interested in as related. |
141 | while (!typeQueue.isEmpty()) { |
142 | const QJsonObject classDef = typeQueue.dequeue(); |
143 | |
144 | const auto classInfos = classDef.value(key: classInfosKey).toArray(); |
145 | for (const QJsonValue &classInfo : classInfos) { |
146 | const QJsonObject obj = classInfo.toObject(); |
147 | if (obj.value(key: nameKey).toString() == qmlAttachedName) { |
148 | addType(obj.value(key: valueKey).toString()); |
149 | } else if (obj.value(key: nameKey).toString() == qmlForeignName) { |
150 | const QString foreignClassName = obj.value(key: valueKey).toString(); |
151 | if (const QJsonObject *other = QmlTypesClassDescription::findType( |
152 | types: foreignTypes, name: foreignClassName)) { |
153 | const auto otherSupers = other->value(key: superClassesKey).toArray(); |
154 | if (!otherSupers.isEmpty()) { |
155 | const QJsonObject otherSuperObject = otherSupers.first().toObject(); |
156 | if (otherSuperObject.value(key: accessKey).toString() == publicAccess) |
157 | addType(otherSuperObject.value(key: nameKey).toString()); |
158 | } |
159 | |
160 | const auto otherClassInfos = other->value(key: classInfosKey).toArray(); |
161 | for (const QJsonValue &otherClassInfo : otherClassInfos) { |
162 | const QJsonObject obj = otherClassInfo.toObject(); |
163 | if (obj.value(key: nameKey).toString() == qmlAttachedName) { |
164 | addType(obj.value(key: valueKey).toString()); |
165 | break; |
166 | } |
167 | // No, you cannot chain QML_FOREIGN declarations. Sorry. |
168 | } |
169 | break; |
170 | } |
171 | } |
172 | } |
173 | |
174 | const auto supers = classDef.value(key: superClassesKey).toArray(); |
175 | if (!supers.isEmpty()) { |
176 | const QJsonObject superObject = supers.first().toObject(); |
177 | if (superObject.value(key: accessKey).toString() == publicAccess) |
178 | addType(superObject.value(key: nameKey).toString()); |
179 | } |
180 | } |
181 | |
182 | return relatedTypes; |
183 | } |
184 | |
185 | int main(int argc, char **argv) |
186 | { |
187 | // Produce reliably the same output for the same input by disabling QHash's random seeding. |
188 | qSetGlobalQHashSeed(newSeed: 0); |
189 | |
190 | QCoreApplication app(argc, argv); |
191 | QCoreApplication::setApplicationName(QStringLiteral("qmltyperegistrar" )); |
192 | QCoreApplication::setApplicationVersion(QLatin1String(QT_VERSION_STR)); |
193 | |
194 | QCommandLineParser parser; |
195 | parser.addHelpOption(); |
196 | parser.addVersionOption(); |
197 | |
198 | QCommandLineOption outputOption(QStringLiteral("o" )); |
199 | outputOption.setDescription(QStringLiteral("Write output to specified file." )); |
200 | outputOption.setValueName(QStringLiteral("file" )); |
201 | outputOption.setFlags(QCommandLineOption::ShortOptionStyle); |
202 | parser.addOption(commandLineOption: outputOption); |
203 | |
204 | QCommandLineOption privateIncludesOption( |
205 | QStringLiteral("private-includes" ), |
206 | QStringLiteral("Include headers ending in \"_p.h\" using \"#include <private/foo_p.h>\"" |
207 | "rather than \"#include <foo_p.h>\"." )); |
208 | parser.addOption(commandLineOption: privateIncludesOption); |
209 | |
210 | QCommandLineOption importNameOption(QStringLiteral("import-name" )); |
211 | importNameOption.setDescription(QStringLiteral("Name of the module to use for type and module " |
212 | "registrations." )); |
213 | importNameOption.setValueName(QStringLiteral("module name" )); |
214 | parser.addOption(commandLineOption: importNameOption); |
215 | |
216 | QCommandLineOption majorVersionOption(QStringLiteral("major-version" )); |
217 | majorVersionOption.setDescription(QStringLiteral("Major version to use for type and module " |
218 | "registrations." )); |
219 | majorVersionOption.setValueName(QStringLiteral("major version" )); |
220 | parser.addOption(commandLineOption: majorVersionOption); |
221 | |
222 | QCommandLineOption minorVersionOption(QStringLiteral("minor-version" )); |
223 | minorVersionOption.setDescription(QStringLiteral("Minor version to use for module " |
224 | "registration." )); |
225 | minorVersionOption.setValueName(QStringLiteral("minor version" )); |
226 | parser.addOption(commandLineOption: minorVersionOption); |
227 | |
228 | QCommandLineOption pluginTypesOption(QStringLiteral("generate-qmltypes" )); |
229 | pluginTypesOption.setDescription(QStringLiteral("Generate qmltypes into specified file." )); |
230 | pluginTypesOption.setValueName(QStringLiteral("qmltypes file" )); |
231 | parser.addOption(commandLineOption: pluginTypesOption); |
232 | |
233 | QCommandLineOption foreignTypesOption(QStringLiteral("foreign-types" )); |
234 | foreignTypesOption.setDescription(QStringLiteral( |
235 | "Comma separated list of other modules' metatypes files " |
236 | "to consult for foreign types when generating " |
237 | "qmltypes file." )); |
238 | foreignTypesOption.setValueName(QStringLiteral("foreign types" )); |
239 | parser.addOption(commandLineOption: foreignTypesOption); |
240 | |
241 | QCommandLineOption dependenciesOption(QStringLiteral("dependencies" )); |
242 | dependenciesOption.setDescription(QStringLiteral("JSON file with dependencies to be stated in " |
243 | "qmltypes file." )); |
244 | dependenciesOption.setValueName(QStringLiteral("dependencies.json" )); |
245 | parser.addOption(commandLineOption: dependenciesOption); |
246 | |
247 | parser.addPositionalArgument(QStringLiteral("[MOC generated json file]" ), |
248 | QStringLiteral("MOC generated json output." )); |
249 | |
250 | parser.process(app); |
251 | |
252 | FILE *output = stdout; |
253 | QScopedPointer<FILE, ScopedPointerFileCloser> outputFile; |
254 | |
255 | if (parser.isSet(option: outputOption)) { |
256 | QString outputName = parser.value(option: outputOption); |
257 | #if defined(_MSC_VER) |
258 | if (_wfopen_s(&output, reinterpret_cast<const wchar_t *>(outputName.utf16()), L"w" ) != 0) { |
259 | #else |
260 | output = fopen(filename: QFile::encodeName(fileName: outputName).constData(), modes: "w" ); // create output file |
261 | if (!output) { |
262 | #endif |
263 | fprintf(stderr, format: "Error: Cannot open %s for writing\n" , qPrintable(outputName)); |
264 | return EXIT_FAILURE; |
265 | } |
266 | outputFile.reset(other: output); |
267 | } |
268 | |
269 | fprintf(stream: output, |
270 | format: "/****************************************************************************\n" |
271 | "** Generated QML type registration code\n**\n" ); |
272 | fprintf(stream: output, |
273 | format: "** WARNING! All changes made in this file will be lost!\n" |
274 | "*****************************************************************************/\n\n" ); |
275 | fprintf(stream: output, |
276 | format: "#include <QtQml/qqml.h>\n" |
277 | "#include <QtQml/qqmlmoduleregistration.h>\n" ); |
278 | |
279 | QStringList includes; |
280 | QVector<QJsonObject> types; |
281 | QVector<QJsonObject> foreignTypes; |
282 | |
283 | const QString module = parser.value(option: importNameOption); |
284 | const QStringList files = parser.positionalArguments(); |
285 | for (const QString &source: files) { |
286 | QJsonDocument metaObjects; |
287 | { |
288 | QFile f(source); |
289 | if (!f.open(flags: QIODevice::ReadOnly)) { |
290 | fprintf(stderr, format: "Error opening %s for reading\n" , qPrintable(source)); |
291 | return EXIT_FAILURE; |
292 | } |
293 | QJsonParseError error = {.offset: 0, .error: QJsonParseError::NoError}; |
294 | metaObjects = QJsonDocument::fromJson(json: f.readAll(), error: &error); |
295 | if (error.error != QJsonParseError::NoError) { |
296 | fprintf(stderr, format: "Error parsing %s\n" , qPrintable(source)); |
297 | return EXIT_FAILURE; |
298 | } |
299 | } |
300 | |
301 | const bool privateIncludes = parser.isSet(option: privateIncludesOption); |
302 | auto resolvedInclude = [&](const QString &include) { |
303 | return (privateIncludes && include.endsWith(s: QLatin1String("_p.h" ))) |
304 | ? QLatin1String("private/" ) + include |
305 | : include; |
306 | }; |
307 | |
308 | auto processMetaObject = [&](const QJsonObject &metaObject) { |
309 | const QString include = resolvedInclude(metaObject[QLatin1String("inputFile" )].toString()); |
310 | const QJsonArray classes = metaObject[QLatin1String("classes" )].toArray(); |
311 | for (const auto &cls : classes) { |
312 | QJsonObject classDef = cls.toObject(); |
313 | classDef.insert(key: QLatin1String("inputFile" ), value: include); |
314 | |
315 | switch (qmlTypeRegistrationMode(classDef)) { |
316 | case NamespaceRegistration: |
317 | case GadgetRegistration: |
318 | case ObjectRegistration: { |
319 | if (!include.endsWith(s: QLatin1String(".h" )) |
320 | && !include.endsWith(s: QLatin1String(".hpp" )) |
321 | && !include.endsWith(s: QLatin1String(".hxx" )) |
322 | && include.contains(c: QLatin1Char('.'))) { |
323 | fprintf(stderr, |
324 | format: "Class %s is declared in %s, which appears not to be a header.\n" |
325 | "The compilation of its registration to QML may fail.\n" , |
326 | qPrintable(classDef.value(QLatin1String("qualifiedClassName" )) |
327 | .toString()), |
328 | qPrintable(include)); |
329 | } |
330 | includes.append(t: include); |
331 | classDef.insert(key: QLatin1String("registerable" ), value: true); |
332 | |
333 | types.append(t: classDef); |
334 | break; |
335 | } |
336 | case NoRegistration: |
337 | foreignTypes.append(t: classDef); |
338 | break; |
339 | } |
340 | } |
341 | }; |
342 | |
343 | if (metaObjects.isArray()) { |
344 | const QJsonArray metaObjectsArray = metaObjects.array(); |
345 | for (const auto &metaObject : metaObjectsArray) { |
346 | if (!metaObject.isObject()) { |
347 | fprintf(stderr, format: "Error parsing %s: JSON is not an object\n" , |
348 | qPrintable(source)); |
349 | return EXIT_FAILURE; |
350 | } |
351 | |
352 | processMetaObject(metaObject.toObject()); |
353 | } |
354 | } else if (metaObjects.isObject()) { |
355 | processMetaObject(metaObjects.object()); |
356 | } else { |
357 | fprintf(stderr, format: "Error parsing %s: JSON is not an object or an array\n" , |
358 | qPrintable(source)); |
359 | return EXIT_FAILURE; |
360 | } |
361 | } |
362 | |
363 | const QLatin1String qualifiedClassNameKey("qualifiedClassName" ); |
364 | auto sortTypes = [&](QVector<QJsonObject> &types) { |
365 | std::sort(first: types.begin(), last: types.end(), comp: [&](const QJsonObject &a, const QJsonObject &b) { |
366 | return a.value(key: qualifiedClassNameKey).toString() < |
367 | b.value(key: qualifiedClassNameKey).toString(); |
368 | }); |
369 | }; |
370 | |
371 | sortTypes(types); |
372 | |
373 | std::sort(first: includes.begin(), last: includes.end()); |
374 | const auto newEnd = std::unique(first: includes.begin(), last: includes.end()); |
375 | includes.erase(afirst: newEnd, alast: includes.end()); |
376 | |
377 | for (const QString &include : qAsConst(t&: includes)) |
378 | fprintf(stream: output, format: "\n#include <%s>" , qPrintable(include)); |
379 | |
380 | fprintf(stream: output, format: "\n\n" ); |
381 | |
382 | QString moduleAsSymbol = module; |
383 | moduleAsSymbol.replace(before: QLatin1Char('.'), after: QLatin1Char('_')); |
384 | |
385 | const QString functionName = QStringLiteral("qml_register_types_" ) + moduleAsSymbol; |
386 | |
387 | fprintf(stream: output, format: "void %s()\n{" , qPrintable(functionName)); |
388 | const auto majorVersion = parser.value(option: majorVersionOption); |
389 | |
390 | for (const QJsonObject &classDef : qAsConst(t&: types)) { |
391 | if (!classDef.value(key: QLatin1String("registerable" )).toBool()) |
392 | continue; |
393 | |
394 | const QString className = classDef[QLatin1String("qualifiedClassName" )].toString(); |
395 | |
396 | if (classDef.value(key: QLatin1String("namespace" )).toBool()) { |
397 | fprintf(stream: output, format: "\n qmlRegisterNamespaceAndRevisions(&%s::staticMetaObject, \"%s\", %s);" , |
398 | qPrintable(className), qPrintable(module), qPrintable(majorVersion)); |
399 | } else { |
400 | fprintf(stream: output, format: "\n qmlRegisterTypesAndRevisions<%s>(\"%s\", %s);" , |
401 | qPrintable(className), qPrintable(module), qPrintable(majorVersion)); |
402 | } |
403 | } |
404 | |
405 | fprintf(stream: output, format: "\n qmlRegisterModule(\"%s\", %s, %s);" , |
406 | qPrintable(module), qPrintable(majorVersion), |
407 | qPrintable(parser.value(minorVersionOption))); |
408 | fprintf(stream: output, format: "\n}\n" ); |
409 | fprintf(stream: output, format: "\nstatic const QQmlModuleRegistration registration(\"%s\", %s, %s);\n" , |
410 | qPrintable(module), qPrintable(majorVersion), qPrintable(functionName)); |
411 | |
412 | if (!parser.isSet(option: pluginTypesOption)) |
413 | return EXIT_SUCCESS; |
414 | |
415 | if (parser.isSet(option: foreignTypesOption)) { |
416 | const QStringList foreignTypesFiles = parser.value(option: foreignTypesOption) |
417 | .split(sep: QLatin1Char(',')); |
418 | for (const QString &types : foreignTypesFiles) { |
419 | QFile typesFile(types); |
420 | if (!typesFile.open(flags: QIODevice::ReadOnly)) { |
421 | fprintf(stderr, format: "Cannot open foreign types file %s\n" , qPrintable(types)); |
422 | continue; |
423 | } |
424 | |
425 | QJsonParseError error = {.offset: 0, .error: QJsonParseError::NoError}; |
426 | QJsonDocument foreignMetaObjects = QJsonDocument::fromJson(json: typesFile.readAll(), error: &error); |
427 | if (error.error != QJsonParseError::NoError) { |
428 | fprintf(stderr, format: "Error parsing %s\n" , qPrintable(types)); |
429 | continue; |
430 | } |
431 | |
432 | const QJsonArray foreignObjectsArray = foreignMetaObjects.array(); |
433 | for (const auto &metaObject : foreignObjectsArray) { |
434 | if (!metaObject.isObject()) { |
435 | fprintf(stderr, format: "Error parsing %s: JSON is not an object\n" , |
436 | qPrintable(types)); |
437 | continue; |
438 | } |
439 | |
440 | const QString include = metaObject[QLatin1String("inputFile" )].toString(); |
441 | const QJsonArray classes = metaObject[QLatin1String("classes" )].toArray(); |
442 | for (const auto &cls : classes) { |
443 | QJsonObject classDef = cls.toObject(); |
444 | classDef.insert(key: QLatin1String("inputFile" ), value: include); |
445 | foreignTypes.append(t: classDef); |
446 | } |
447 | } |
448 | } |
449 | } |
450 | |
451 | sortTypes(foreignTypes); |
452 | types += foreignRelatedTypes(types, foreignTypes); |
453 | sortTypes(types); |
454 | |
455 | QmlTypesCreator creator; |
456 | creator.setOwnTypes(std::move(types)); |
457 | creator.setForeignTypes(std::move(foreignTypes)); |
458 | creator.setModule(module); |
459 | creator.setMajorVersion(parser.value(option: majorVersionOption).toInt()); |
460 | |
461 | creator.generate(outFileName: parser.value(option: pluginTypesOption), dependenciesFileName: parser.value(option: dependenciesOption)); |
462 | return EXIT_SUCCESS; |
463 | } |
464 | |