// Copyright (C) 2019 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; using AutoGenHeaderMap = QMap; using AutoGenSourcesList = QList; static bool readAutogenInfoJson(AutoGenHeaderMap &headers, AutoGenSourcesList &sources, QStringList &headerExts, const QString &autoGenInfoJsonPath) { QFile file(autoGenInfoJsonPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { fprintf(stderr, "Could not open: %s\n", qPrintable(autoGenInfoJsonPath)); return false; } const QByteArray contents = file.readAll(); file.close(); QJsonParseError error; QJsonDocument doc = QJsonDocument::fromJson(contents, &error); if (error.error != QJsonParseError::NoError) { fprintf(stderr, "Failed to parse json file: %s\n", qPrintable(autoGenInfoJsonPath)); return false; } QJsonObject rootObject = doc.object(); QJsonValue headersValue = rootObject.value("HEADERS"_L1); QJsonValue sourcesValue = rootObject.value("SOURCES"_L1); QJsonValue headerExtValue = rootObject.value("HEADER_EXTENSIONS"_L1); if (!headersValue.isArray() || !sourcesValue.isArray() || !headerExtValue.isArray()) { fprintf(stderr, "%s layout does not match the expected layout. This most likely means that file " "format changed or this file is not a product of CMake's AutoGen process.\n", qPrintable(autoGenInfoJsonPath)); return false; } QJsonArray headersArray = headersValue.toArray(); QJsonArray sourcesArray = sourcesValue.toArray(); QJsonArray headerExtArray = headerExtValue.toArray(); for (const QJsonValue value : headersArray) { QJsonArray entry_array = value.toArray(); if (entry_array.size() > 2) { // Array[0] : header path // Array[2] : Location of the generated moc file for this header // if no source file includes it headers.insert(entry_array[0].toString(), entry_array[2].toString()); } } sources.reserve(sourcesArray.size()); for (const QJsonValue value : sourcesArray) { QJsonArray entry_array = value.toArray(); if (entry_array.size() > 1) { sources.push_back(entry_array[0].toString()); } } headerExts.reserve(headerExtArray.size()); for (const QJsonValue value : headerExtArray) { headerExts.push_back(value.toString()); } return true; } struct ParseCacheEntry { QStringList mocFiles; QStringList mocIncludes; }; using ParseCacheMap = QMap; static bool readParseCache(ParseCacheMap &entries, const QString &parseCacheFilePath) { QFile file(parseCacheFilePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { fprintf(stderr, "Could not open: %s\n", qPrintable(parseCacheFilePath)); return false; } QString source; QStringList mocEntries; QStringList mocIncludes; // File format // .... // header/source path N // mmc:Q_OBJECT| mcc:Q_GADGET # This file has been mocked // miu:moc_....cpp # Path of the moc.cpp file generated for the above file // relative to TARGET_BINARY_DIR/TARGET_autgen/include directory. Not // present for headers. // mid: ....moc # Path of .moc file generated for the above file relative // to TARGET_BINARY_DIR/TARGET_autogen/include directory. // uic: UI related info, ignored // mdp: Moc dependencies, ignored // udp: UI dependencies, ignored // header/source path N + 1 // .... QTextStream textStream(&file); const QString mmcKey = QString(" mmc:"_L1); const QString miuKey = QString(" miu:"_L1); const QString uicKey = QString(" uic:"_L1); const QString midKey = QString(" mid:"_L1); const QString mdpKey = QString(" mdp:"_L1); const QString udpKey = QString(" udp:"_L1); QString line; bool mmc_key_found = false; while (textStream.readLineInto(&line)) { if (!line.startsWith(u' ')) { if (!mocEntries.isEmpty() || mmc_key_found || !mocIncludes.isEmpty()) { entries.insert(source, ParseCacheEntry { std::move(mocEntries), std::move(mocIncludes) }); source.clear(); mocEntries = {}; mocIncludes = {}; mmc_key_found = false; } source = line; } else if (line.startsWith(mmcKey)) { mmc_key_found = true; } else if (line.startsWith(miuKey)) { mocIncludes.push_back(line.right(line.size() - miuKey.size())); } else if (line.startsWith(midKey)) { mocEntries.push_back(line.right(line.size() - midKey.size())); } else if (line.startsWith(uicKey) || line.startsWith(mdpKey) || line.startsWith(udpKey)) { // nothing to do ignore continue; } else { fprintf(stderr, "Unhandled line entry \"%s\" in %s\n", qPrintable(line), qPrintable(parseCacheFilePath)); return false; } } // Check if last entry has any data left to processed if (!mocEntries.isEmpty() || !mocIncludes.isEmpty() || mmc_key_found) { entries.insert(source, ParseCacheEntry { std::move(mocEntries), std::move(mocIncludes) }); } file.close(); return true; } static bool writeJsonFiles(const QList &fileList, const QString &fileListFilePath, const QString ×tampFilePath) { QFile timestampFile(timestampFilePath); if (!timestampFile.open(QIODevice::ReadWrite)) { fprintf(stderr, "Could not open: %s\n", qPrintable(timestampFilePath)); return false; } qint64 timestamp = std::numeric_limits::min(); if (timestampFile.size() == sizeof(timestamp)) timestampFile.read(reinterpret_cast(×tamp), sizeof(timestamp)); // Check if any of the metatype json files produced by automoc is newer than the last file // processed by cmake_automoc parser for (const auto &jsonFile : fileList) { const qint64 jsonFileLastModified = QFileInfo(jsonFile).lastModified(QTimeZone::UTC).toMSecsSinceEpoch(); if (jsonFileLastModified > timestamp) { timestamp = jsonFileLastModified; } } if (timestamp != std::numeric_limits::min() || !QFile::exists(fileListFilePath)) { QFile file(fileListFilePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) { fprintf(stderr, "Could not open: %s\n", qPrintable(fileListFilePath)); return false; } QTextStream textStream(&file); for (const auto &jsonFile : fileList) { textStream << jsonFile << Qt::endl; } textStream.flush(); // Update the timestamp according the newest json file timestamp. timestampFile.resize(0); timestampFile.write(reinterpret_cast(×tamp), sizeof(timestamp)); } return true; } int main(int argc, char **argv) { QCoreApplication app(argc, argv); QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("Qt CMake Autogen parser tool")); parser.addHelpOption(); parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); QCommandLineOption outputFileOption(QStringLiteral("output-file-path")); outputFileOption.setDescription( QStringLiteral("Output file where the meta type file list will be written.")); outputFileOption.setValueName(QStringLiteral("output file")); parser.addOption(outputFileOption); QCommandLineOption cmakeAutogenCacheFileOption(QStringLiteral("cmake-autogen-cache-file")); cmakeAutogenCacheFileOption.setDescription( QStringLiteral("Location of the CMake AutoGen ParseCache.txt file.")); cmakeAutogenCacheFileOption.setValueName(QStringLiteral("CMake AutoGen ParseCache.txt file")); parser.addOption(cmakeAutogenCacheFileOption); QCommandLineOption cmakeAutogenInfoFileOption(QStringLiteral("cmake-autogen-info-file")); cmakeAutogenInfoFileOption.setDescription( QStringLiteral("Location of the CMake AutoGen AutogenInfo.json file.")); cmakeAutogenInfoFileOption.setValueName(QStringLiteral("CMake AutoGen AutogenInfo.json file")); parser.addOption(cmakeAutogenInfoFileOption); QCommandLineOption cmakeAutogenIncludeDirOption( QStringLiteral("cmake-autogen-include-dir-path")); cmakeAutogenIncludeDirOption.setDescription( QStringLiteral("Location of the CMake AutoGen include directory.")); cmakeAutogenIncludeDirOption.setValueName(QStringLiteral("CMake AutoGen include directory")); parser.addOption(cmakeAutogenIncludeDirOption); QCommandLineOption isMultiConfigOption( QStringLiteral("cmake-multi-config")); isMultiConfigOption.setDescription( QStringLiteral("Set this option when using CMake with a multi-config generator")); parser.addOption(isMultiConfigOption); QCommandLineOption timestampFilePathOption(QStringLiteral("timestamp-file-path")); timestampFilePathOption.setDescription( QStringLiteral("The path to a timestamp file that determines whether the output" " file needs to be updated.")); timestampFilePathOption.setValueName(QStringLiteral("timestamp file")); parser.addOption(timestampFilePathOption); QStringList arguments = QCoreApplication::arguments(); parser.process(arguments); if (!parser.isSet(outputFileOption) || !parser.isSet(cmakeAutogenInfoFileOption) || !parser.isSet(cmakeAutogenCacheFileOption) || !parser.isSet(cmakeAutogenIncludeDirOption)) { parser.showHelp(1); return EXIT_FAILURE; } // Read source files from AutogenInfo.json AutoGenHeaderMap autoGenHeaders; AutoGenSourcesList autoGenSources; QStringList headerExtList; if (!readAutogenInfoJson(autoGenHeaders, autoGenSources, headerExtList, parser.value(cmakeAutogenInfoFileOption))) { return EXIT_FAILURE; } ParseCacheMap parseCacheEntries; if (!readParseCache(parseCacheEntries, parser.value(cmakeAutogenCacheFileOption))) { return EXIT_FAILURE; } const QString cmakeIncludeDir = parser.value(cmakeAutogenIncludeDirOption); // Algorithm description // 1) For each source from the AutoGenSources list check if there is a parse // cache entry. // 1a) If an entry was wound there exists an moc_...cpp file somewhere. // Remove the header file from the AutoGenHeader files // 1b) For every matched source entry, check the moc includes as it is // possible for a source file to include moc files from other headers. // Remove the header from AutoGenHeaders // 2) For every remaining header in AutoGenHeaders, check if there is an // entry for it in the parse cache. Use the value for the location of the // moc.json file QList jsonFileList; QDir dir(cmakeIncludeDir); jsonFileList.reserve(autoGenSources.size()); // 1) Process sources for (const auto &source : autoGenSources) { auto it = parseCacheEntries.find(source); if (it == parseCacheEntries.end()) { continue; } const QFileInfo fileInfo(source); const QString base = fileInfo.path() + fileInfo.completeBaseName(); // 1a) erase header for (const auto &ext : headerExtList) { const QString headerPath = base + u'.' + ext; auto it = autoGenHeaders.find(headerPath); if (it != autoGenHeaders.end()) { autoGenHeaders.erase(it); break; } } // Add extra moc files for (const auto &mocFile : it.value().mocFiles) jsonFileList.push_back(dir.filePath(mocFile) + ".json"_L1); // Add main moc files for (const auto &mocFile : it.value().mocIncludes) { jsonFileList.push_back(dir.filePath(mocFile) + ".json"_L1); // 1b) Locate this header and delete it constexpr int mocKeyLen = 4; // length of "moc_" const QString headerBaseName = QFileInfo(mocFile.right(mocFile.size() - mocKeyLen)).completeBaseName(); bool breakFree = false; for (auto &ext : headerExtList) { const QString headerSuffix = headerBaseName + u'.' + ext; for (auto it = autoGenHeaders.begin(); it != autoGenHeaders.end(); ++it) { if (it.key().endsWith(headerSuffix) && QFileInfo(it.key()).completeBaseName() == headerBaseName) { autoGenHeaders.erase(it); breakFree = true; break; } } if (breakFree) { break; } } } } // 2) Process headers const bool isMultiConfig = parser.isSet(isMultiConfigOption); for (auto mapIt = autoGenHeaders.begin(); mapIt != autoGenHeaders.end(); ++mapIt) { auto it = parseCacheEntries.find(mapIt.key()); if (it == parseCacheEntries.end()) { continue; } const QString pathPrefix = !isMultiConfig ? QStringLiteral("../") : QString(); const QString jsonPath = dir.filePath(pathPrefix + mapIt.value() + ".json"_L1); jsonFileList.push_back(jsonPath); } // Sort for consistent checks across runs jsonFileList.sort(); // Read Previous file list (if any) if (!writeJsonFiles(jsonFileList, parser.value(outputFileOption), parser.value(timestampFilePathOption))) { return EXIT_FAILURE; } return EXIT_SUCCESS; } QT_END_NAMESPACE