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 tools applications 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 <QtCore/qglobal.h>
30
31#include <cstdio>
32#include <cstdlib>
33
34#include <qcommandlineoption.h>
35#include <qcommandlineparser.h>
36#include <qcoreapplication.h>
37#include <qdebug.h>
38#include <qdir.h>
39#include <qfile.h>
40#include <qhash.h>
41#include <qjsonarray.h>
42#include <qjsondocument.h>
43#include <qjsonobject.h>
44#include <qlist.h>
45#include <qmap.h>
46#include <qset.h>
47#include <qstring.h>
48#include <qstack.h>
49
50QT_BEGIN_NAMESPACE
51
52using AutoGenHeaderMap = QMap<QString, QString>;
53using AutoGenSourcesList = QList<QString>;
54
55static bool readAutogenInfoJson(AutoGenHeaderMap &headers, AutoGenSourcesList &sources,
56 QStringList &headerExts, const QString &autoGenInfoJsonPath)
57{
58 QFile file(autoGenInfoJsonPath);
59 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
60 fprintf(stderr, "Could not open: %s\n", qPrintable(autoGenInfoJsonPath));
61 return false;
62 }
63
64 const QByteArray contents = file.readAll();
65 file.close();
66
67 QJsonParseError error;
68 QJsonDocument doc = QJsonDocument::fromJson(contents, &error);
69
70 if (error.error != QJsonParseError::NoError) {
71 fprintf(stderr, "Failed to parse json file: %s\n", qPrintable(autoGenInfoJsonPath));
72 return false;
73 }
74
75 QJsonObject rootObject = doc.object();
76 QJsonValue headersValue = rootObject.value(QLatin1String("HEADERS"));
77 QJsonValue sourcesValue = rootObject.value(QLatin1String("SOURCES"));
78 QJsonValue headerExtValue = rootObject.value(QLatin1String("HEADER_EXTENSIONS"));
79
80 if (!headersValue.isArray() || !sourcesValue.isArray() || !headerExtValue.isArray()) {
81 fprintf(stderr,
82 "%s layout does not match the expected layout. This most likely means that file "
83 "format changed or this file is not a product of CMake's AutoGen process.\n",
84 qPrintable(autoGenInfoJsonPath));
85 return false;
86 }
87
88 QJsonArray headersArray = headersValue.toArray();
89 QJsonArray sourcesArray = sourcesValue.toArray();
90 QJsonArray headerExtArray = headerExtValue.toArray();
91
92 for (const QJsonValue value : headersArray) {
93 QJsonArray entry_array = value.toArray();
94 if (entry_array.size() > 2) {
95 // Array[0] : header path
96 // Array[2] : Location of the generated moc file for this header
97 // if no source file includes it
98 headers.insert(entry_array[0].toString(), entry_array[2].toString());
99 }
100 }
101
102 sources.reserve(sourcesArray.size());
103 for (const QJsonValue value : sourcesArray) {
104 QJsonArray entry_array = value.toArray();
105 if (entry_array.size() > 1) {
106 sources.push_back(entry_array[0].toString());
107 }
108 }
109
110 headerExts.reserve(headerExtArray.size());
111 for (const QJsonValue value : headerExtArray) {
112 headerExts.push_back(value.toString());
113 }
114
115 return true;
116}
117
118struct ParseCacheEntry
119{
120 QStringList mocFiles;
121 QStringList mocIncludes;
122};
123
124using ParseCacheMap = QMap<QString, ParseCacheEntry>;
125
126static bool readParseCache(ParseCacheMap &entries, const QString &parseCacheFilePath)
127{
128
129 QFile file(parseCacheFilePath);
130 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
131 fprintf(stderr, "Could not open: %s\n", qPrintable(parseCacheFilePath));
132 return false;
133 }
134
135 QString source;
136 QStringList mocEntries;
137 QStringList mocIncludes;
138
139 // File format
140 // ....
141 // header/source path N
142 // mmc:Q_OBJECT| mcc:Q_GADGET # This file has been mocked
143 // miu:moc_....cpp # Path of the moc.cpp file generated for the above file
144 // relative to TARGET_BINARY_DIR/TARGET_autgen/include directory. Not
145 // present for headers.
146 // mid: ....moc # Path of .moc file generated for the above file relative
147 // to TARGET_BINARY_DIR/TARGET_autogen/include directory.
148 // uic: UI related info, ignored
149 // mdp: Moc dependencies, ignored
150 // udp: UI dependencies, ignored
151 // header/source path N + 1
152 // ....
153
154 QTextStream textStream(&file);
155 const QString mmcKey = QString(QLatin1String(" mmc:"));
156 const QString miuKey = QString(QLatin1String(" miu:"));
157 const QString uicKey = QString(QLatin1String(" uic:"));
158 const QString midKey = QString(QLatin1String(" mid:"));
159 const QString mdpKey = QString(QLatin1String(" mdp:"));
160 const QString udpKey = QString(QLatin1String(" udp:"));
161 QString line;
162 bool mmc_key_found = false;
163 while (textStream.readLineInto(&line)) {
164 if (!line.startsWith(QLatin1Char(' '))) {
165 if (!mocEntries.isEmpty() || mmc_key_found || !mocIncludes.isEmpty()) {
166 entries.insert(source,
167 ParseCacheEntry { std::move(mocEntries), std::move(mocIncludes) });
168 source.clear();
169 mmc_key_found = false;
170 }
171 source = line;
172 } else if (line.startsWith(mmcKey)) {
173 mmc_key_found = true;
174 } else if (line.startsWith(miuKey)) {
175 mocIncludes.push_back(line.right(line.size() - miuKey.size()));
176 } else if (line.startsWith(midKey)) {
177 mocEntries.push_back(line.right(line.size() - midKey.size()));
178 } else if (line.startsWith(uicKey) || line.startsWith(mdpKey) || line.startsWith(udpKey)) {
179 // nothing to do ignore
180 continue;
181 } else {
182 fprintf(stderr, "Unhandled line entry \"%s\" in %s\n", qPrintable(line),
183 qPrintable(parseCacheFilePath));
184 return false;
185 }
186 }
187
188 // Check if last entry has any data left to processed
189 if (!mocEntries.isEmpty() || !mocIncludes.isEmpty() || mmc_key_found) {
190 entries.insert(source, ParseCacheEntry { std::move(mocEntries), std::move(mocIncludes) });
191 }
192
193 file.close();
194 return true;
195}
196
197static bool readJsonFiles(QList<QString> &entries, const QString &filePath)
198{
199
200 QFile file(filePath);
201 if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
202 fprintf(stderr, "Could not open: %s\n", qPrintable(filePath));
203 return false;
204 }
205
206 QTextStream textStream(&file);
207 QString line;
208 while (textStream.readLineInto(&line)) {
209 entries.push_back(line);
210 }
211 file.close();
212 return true;
213}
214
215static bool writeJsonFiles(const QList<QString> &fileList, const QString &fileListFilePath)
216{
217 QFile file(fileListFilePath);
218 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
219 fprintf(stderr, "Could not open: %s\n", qPrintable(fileListFilePath));
220 return false;
221 }
222
223 QTextStream textStream(&file);
224 for (const auto &file : fileList) {
225 textStream << file << Qt::endl;
226 }
227
228 file.close();
229 return true;
230}
231
232int main(int argc, char **argv)
233{
234
235 QCoreApplication app(argc, argv);
236 QCommandLineParser parser;
237 parser.setApplicationDescription(QStringLiteral("Qt CMake Autogen parser tool"));
238
239 parser.addHelpOption();
240 parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions);
241
242 QCommandLineOption outputFileOption(QStringLiteral("output-file-path"));
243 outputFileOption.setDescription(
244 QStringLiteral("Output file where the meta type file list will be written."));
245 outputFileOption.setValueName(QStringLiteral("output file"));
246 parser.addOption(outputFileOption);
247
248 QCommandLineOption cmakeAutogenCacheFileOption(QStringLiteral("cmake-autogen-cache-file"));
249 cmakeAutogenCacheFileOption.setDescription(
250 QStringLiteral("Location of the CMake AutoGen ParseCache.txt file."));
251 cmakeAutogenCacheFileOption.setValueName(QStringLiteral("CMake AutoGen ParseCache.txt file"));
252 parser.addOption(cmakeAutogenCacheFileOption);
253
254 QCommandLineOption cmakeAutogenInfoFileOption(QStringLiteral("cmake-autogen-info-file"));
255 cmakeAutogenInfoFileOption.setDescription(
256 QStringLiteral("Location of the CMake AutoGen AutogenInfo.json file."));
257 cmakeAutogenInfoFileOption.setValueName(QStringLiteral("CMake AutoGen AutogenInfo.json file"));
258 parser.addOption(cmakeAutogenInfoFileOption);
259
260 QCommandLineOption cmakeAutogenIncludeDirOption(
261 QStringLiteral("cmake-autogen-include-dir-path"));
262 cmakeAutogenIncludeDirOption.setDescription(
263 QStringLiteral("Location of the CMake AutoGen include directory."));
264 cmakeAutogenIncludeDirOption.setValueName(QStringLiteral("CMake AutoGen include directory"));
265 parser.addOption(cmakeAutogenIncludeDirOption);
266
267 QCommandLineOption isMultiConfigOption(
268 QStringLiteral("cmake-multi-config"));
269 isMultiConfigOption.setDescription(
270 QStringLiteral("Set this option when using CMake with a multi-config generator"));
271 parser.addOption(isMultiConfigOption);
272
273 QStringList arguments = QCoreApplication::arguments();
274 parser.process(arguments);
275
276 if (!parser.isSet(outputFileOption) || !parser.isSet(cmakeAutogenInfoFileOption)
277 || !parser.isSet(cmakeAutogenCacheFileOption)
278 || !parser.isSet(cmakeAutogenIncludeDirOption)) {
279 parser.showHelp(1);
280 return EXIT_FAILURE;
281 }
282
283 // Read source files from AutogenInfo.json
284 AutoGenHeaderMap autoGenHeaders;
285 AutoGenSourcesList autoGenSources;
286 QStringList headerExtList;
287 if (!readAutogenInfoJson(autoGenHeaders, autoGenSources, headerExtList,
288 parser.value(cmakeAutogenInfoFileOption))) {
289 return EXIT_FAILURE;
290 }
291
292 ParseCacheMap parseCacheEntries;
293 if (!readParseCache(parseCacheEntries, parser.value(cmakeAutogenCacheFileOption))) {
294 return EXIT_FAILURE;
295 }
296
297 const QString cmakeIncludeDir = parser.value(cmakeAutogenIncludeDirOption);
298
299 // Algorithm description
300 // 1) For each source from the AutoGenSources list check if there is a parse
301 // cache entry.
302 // 1a) If an entry was wound there exists an moc_...cpp file somewhere.
303 // Remove the header file from the AutoGenHeader files
304 // 1b) For every matched source entry, check the moc includes as it is
305 // possible for a source file to include moc files from other headers.
306 // Remove the header from AutoGenHeaders
307 // 2) For every remaining header in AutoGenHeaders, check if there is an
308 // entry for it in the parse cache. Use the value for the location of the
309 // moc.json file
310
311 QList<QString> jsonFileList;
312 QDir dir(cmakeIncludeDir);
313 jsonFileList.reserve(autoGenSources.size());
314
315 // 1) Process sources
316 for (const auto &source : autoGenSources) {
317 auto it = parseCacheEntries.find(source);
318 if (it == parseCacheEntries.end()) {
319 continue;
320 }
321
322 const QFileInfo fileInfo(source);
323 const QString base = fileInfo.path() + fileInfo.completeBaseName();
324 // 1a) erase header
325 for (const auto &ext : headerExtList) {
326 const QString headerPath = base + QLatin1Char('.') + ext;
327 auto it = autoGenHeaders.find(headerPath);
328 if (it != autoGenHeaders.end()) {
329 autoGenHeaders.erase(it);
330 break;
331 }
332 }
333 // Add extra moc files
334 for (const auto &mocFile : it.value().mocFiles) {
335 jsonFileList.push_back(dir.filePath(mocFile) + QLatin1String(".json"));
336 }
337 // Add main moc files
338 for (const auto &mocFile : it.value().mocIncludes) {
339 jsonFileList.push_back(dir.filePath(mocFile) + QLatin1String(".json"));
340 // 1b) Locate this header and delete it
341 constexpr int mocKeyLen = 4; // length of "moc_"
342 const QString headerBaseName =
343 QFileInfo(mocFile.right(mocFile.size() - mocKeyLen)).completeBaseName();
344 bool breakFree = false;
345 for (auto &ext : headerExtList) {
346 const QString headerSuffix = headerBaseName + QLatin1Char('.') + ext;
347 for (auto it = autoGenHeaders.begin(); it != autoGenHeaders.end(); ++it) {
348 if (it.key().endsWith(headerSuffix)
349 && QFileInfo(it.key()).completeBaseName() == headerBaseName) {
350 autoGenHeaders.erase(it);
351 breakFree = true;
352 break;
353 }
354 }
355 if (breakFree) {
356 break;
357 }
358 }
359 }
360 }
361
362 // 2) Process headers
363 const bool isMultiConfig = parser.isSet(isMultiConfigOption);
364 for (auto mapIt = autoGenHeaders.begin(); mapIt != autoGenHeaders.end(); ++mapIt) {
365 auto it = parseCacheEntries.find(mapIt.key());
366 if (it == parseCacheEntries.end()) {
367 continue;
368 }
369 const QString pathPrefix = !isMultiConfig
370 ? QStringLiteral("../")
371 : QString();
372 const QString jsonPath =
373 dir.filePath(pathPrefix + mapIt.value() + QLatin1String(".json"));
374 jsonFileList.push_back(jsonPath);
375 }
376
377 // Sort for consistent checks across runs
378 jsonFileList.sort();
379
380 // Read Previous file list (if any)
381 const QString fileListFilePath = parser.value(outputFileOption);
382 QList<QString> previousList;
383 QFile prev_file(fileListFilePath);
384
385 // Only try to open file if it exists to avoid error messages
386 if (prev_file.exists()) {
387 (void)readJsonFiles(previousList, fileListFilePath);
388 }
389
390 if (previousList != jsonFileList || !QFile(fileListFilePath).exists()) {
391 if (!writeJsonFiles(jsonFileList, fileListFilePath)) {
392 return EXIT_FAILURE;
393 }
394 }
395
396 return EXIT_SUCCESS;
397}
398
399QT_END_NAMESPACE
400