diff options
Diffstat (limited to 'src/tools/syncqt/main.cpp')
-rw-r--r-- | src/tools/syncqt/main.cpp | 553 |
1 files changed, 409 insertions, 144 deletions
diff --git a/src/tools/syncqt/main.cpp b/src/tools/syncqt/main.cpp index b4c37ec626..5df7b03fd5 100644 --- a/src/tools/syncqt/main.cpp +++ b/src/tools/syncqt/main.cpp @@ -7,8 +7,7 @@ * - Header file that contains the module version information, and named as <module>Vesion * - LD version script if applicable * - Aliases or copies of the header files sorted by the generic Qt-types: public/private/qpa - * and stored in the corresponding directories. Also copies the aliases to the framework-specific - * directories. + * and stored in the corresponding directories. * Also the tool executes conformity checks on each header file if applicable, to make sure they * follow rules that are relevant for their header type. * The tool can be run in two modes: with either '-all' or '-headers' options specified. Depending @@ -19,6 +18,7 @@ #include <iostream> #include <fstream> #include <string> +#include <string_view> #include <cstring> #include <sstream> #include <filesystem> @@ -27,6 +27,7 @@ #include <regex> #include <map> #include <set> +#include <stdexcept> #include <array> enum ErrorCodes { @@ -42,15 +43,19 @@ enum HeaderChecks { PrivateHeaderChecks = 2, /* Checks if the public header includes a private header */ IncludeChecks = 4, /* Checks if the real header file but not an alias is included */ WeMeantItChecks = 8, /* Checks if private header files contains 'We meant it' disclaimer */ - CriticalChecks = PrivateHeaderChecks, /* Checks that lead to the fatal error of the sync - process */ - AllChecks = NamespaceChecks | PrivateHeaderChecks | IncludeChecks | WeMeantItChecks, + PragmaOnceChecks = 16, + /* Checks that lead to the fatal error of the sync process: */ + CriticalChecks = PrivateHeaderChecks | PragmaOnceChecks, + AllChecks = NamespaceChecks | CriticalChecks | IncludeChecks | WeMeantItChecks, }; constexpr int LinkerScriptCommentAlignment = 55; static const std::regex GlobalHeaderRegex("^q(.*)global\\.h$"); +constexpr std::string_view ErrorMessagePreamble = "ERROR: "; +constexpr std::string_view WarningMessagePreamble = "WARNING: "; + // This comparator is used to sort include records in master header. // It's used to put q.*global.h file to the top of the list and sort all other files alphabetically. bool MasterHeaderIncludeComparator(const std::string &a, const std::string &b) @@ -86,6 +91,33 @@ std::string asciiToUpper(std::string s) return s; } +bool parseVersion(const std::string &version, int &major, int &minor) +{ + const size_t separatorPos = version.find('.'); + if (separatorPos == std::string::npos || separatorPos == (version.size() - 1) + || separatorPos == 0) + return false; + + try { + size_t pos = 0; + major = std::stoi(version.substr(0, separatorPos), &pos); + if (pos != separatorPos) + return false; + + const size_t nextPart = separatorPos + 1; + pos = 0; + minor = std::stoi(version.substr(nextPart), &pos); + if (pos != (version.size() - nextPart)) + return false; + } catch (const std::invalid_argument &) { + return false; + } catch (const std::out_of_range &) { + return false; + } + + return true; +} + class DummyOutputStream : public std::ostream { struct : public std::streambuf @@ -104,12 +136,46 @@ void printInternalError() << std::endl; } +void printFilesystemError(const std::filesystem::filesystem_error &fserr, std::string_view errorMsg) +{ + std::cerr << errorMsg << ": " << fserr.path1() << ".\n" + << fserr.what() << "(" << fserr.code().value() << ")" << std::endl; +} + std::filesystem::path normilizedPath(const std::string &path) { - return std::filesystem::path(std::filesystem::absolute(path).generic_string()); + try { + auto result = std::filesystem::path(std::filesystem::weakly_canonical(path).generic_string()); + return result; + } catch (const std::filesystem::filesystem_error &fserr) { + printFilesystemError(fserr, "Unable to normalize path"); + throw; + } } + +bool createDirectories(const std::string &path, std::string_view errorMsg, bool *exists = nullptr) +{ + bool result = true; + try { + if (!std::filesystem::exists(path)) { + if (exists) + *exists = false; + std::filesystem::create_directories(path); + } else { + if (exists) + *exists = true; + } + } catch (const std::filesystem::filesystem_error &fserr) { + result = false; + std::cerr << errorMsg << ": " << path << ".\n" + << fserr.code().message() << "(" << fserr.code().value() << "):" << fserr.what() + << std::endl; + } + return result; } +} // namespace utils + using FileStamp = std::filesystem::file_time_type; class CommandLineOptions @@ -141,10 +207,12 @@ public: const std::string &privateIncludeDir() const { return m_privateIncludeDir; } - const std::string &frameworkIncludeDir() const { return m_frameworkIncludeDir; } - const std::string &qpaIncludeDir() const { return m_qpaIncludeDir; } + const std::string &rhiIncludeDir() const { return m_rhiIncludeDir; } + + const std::string &ssgIncludeDir() const { return m_ssgIncludeDir; } + const std::string &stagingDir() const { return m_stagingDir; } const std::string &versionScriptFile() const { return m_versionScriptFile; } @@ -153,6 +221,10 @@ public: const std::regex &qpaHeadersRegex() const { return m_qpaHeadersRegex; } + const std::regex &rhiHeadersRegex() const { return m_rhiHeadersRegex; } + + const std::regex &ssgHeadersRegex() const { return m_ssgHeadersRegex; } + const std::regex &privateHeadersRegex() const { return m_privateHeadersRegex; } const std::regex &publicNamespaceRegex() const { return m_publicNamespaceRegex; } @@ -163,8 +235,6 @@ public: bool scanAllMode() const { return m_scanAllMode; } - bool isFramework() const { return m_isFramework; } - bool isInternal() const { return m_isInternal; } bool isNonQtModule() const { return m_isNonQtModule; } @@ -184,10 +254,9 @@ public: void printHelp() const { std::cout << "Usage: syncqt -sourceDir <dir> -binaryDir <dir> -module <module name>" - " -includeDir <dir> -privateIncludeDir <dir> -qpaIncludeDir <dir>" + " -includeDir <dir> -privateIncludeDir <dir> -qpaIncludeDir <dir> -rhiIncludeDir <dir> -ssgIncludeDir <dir>" " -stagingDir <dir> <-headers <header list>|-all> [-debug]" - " [-versionScript <path>] [-qpaHeadersFilter <regex>]" - " [-framework [-frameworkIncludeDir <dir>]]" + " [-versionScript <path>] [-qpaHeadersFilter <regex>] [-rhiHeadersFilter <regex>]" " [-knownModules <module1> <module2>... <moduleN>]" " [-nonQt] [-internal] [-copy]\n" "" @@ -208,6 +277,10 @@ public: " generated private header files.\n" " -qpaIncludeDir Module include directory for the \n" " generated QPA header files.\n" + " -rhiIncludeDir Module include directory for the \n" + " generated RHI header files.\n" + " -ssgIncludeDir Module include directory for the \n" + " generated SSG header files.\n" " -stagingDir Temporary staging directory to collect\n" " artifacts that need to be installed.\n" " -knownModules list of known modules. syncqt uses the\n" @@ -221,16 +294,16 @@ public: " from the list of 'headers'.\n" " -qpaHeadersFilter Regex that filters qpa header files from.\n" " the list of 'headers'.\n" + " -rhiHeadersFilter Regex that filters rhi header files from.\n" + " the list of 'headers'.\n" + " -ssgHeadersFilter Regex that filters ssg files from.\n" + " the list of 'headers'.\n" " -publicNamespaceFilter Symbols that are in the specified\n" " namespace.\n" " are treated as public symbols.\n" " -versionScript Generate linker version script by\n" " provided path.\n" " -debug Enable debug output.\n" - " -framework Indicates that module is framework.\n" - " -frameworkIncludeDir The directory to store the framework\n" - " header files.\n" - " E.g. QtCore.framework/Versions/A/Headers\n" " -copy Copy header files instead of creating\n" " aliases.\n" " -minimal Do not create CaMeL case headers for the\n" @@ -258,34 +331,38 @@ private: [[nodiscard]] bool parseArguments(int argc, char *argv[]) { std::string qpaHeadersFilter; + std::string rhiHeadersFilter; + std::string ssgHeadersFilter; std::string privateHeadersFilter; std::string publicNamespaceFilter; - std::set<std::string> generatedHeaders; static std::unordered_map<std::string, CommandLineOption<std::string>> stringArgumentMap = { { "-module", { &m_moduleName } }, { "-sourceDir", { &m_sourceDir } }, { "-binaryDir", { &m_binaryDir } }, { "-privateHeadersFilter", { &privateHeadersFilter, true } }, { "-qpaHeadersFilter", { &qpaHeadersFilter, true } }, + { "-rhiHeadersFilter", { &rhiHeadersFilter, true } }, + { "-ssgHeadersFilter", { &ssgHeadersFilter, true } }, { "-includeDir", { &m_includeDir } }, { "-privateIncludeDir", { &m_privateIncludeDir } }, { "-qpaIncludeDir", { &m_qpaIncludeDir } }, + { "-rhiIncludeDir", { &m_rhiIncludeDir } }, + { "-ssgIncludeDir", { &m_ssgIncludeDir } }, { "-stagingDir", { &m_stagingDir, true } }, { "-versionScript", { &m_versionScriptFile, true } }, - { "-frameworkIncludeDir", { &m_frameworkIncludeDir, true } }, { "-publicNamespaceFilter", { &publicNamespaceFilter, true } }, }; static const std::unordered_map<std::string, CommandLineOption<std::set<std::string>>> listArgumentMap = { { "-headers", { &m_headers, true } }, - { "-generatedHeaders", { &generatedHeaders, true } }, + { "-generatedHeaders", { &m_generatedHeaders, true } }, { "-knownModules", { &m_knownModules, true } }, }; static const std::unordered_map<std::string, CommandLineOption<bool>> boolArgumentMap = { { "-nonQt", { &m_isNonQtModule, true } }, { "-debug", { &m_debug, true } }, - { "-help", { &m_printHelpOnly, true } }, { "-framework", { &m_isFramework, true } }, + { "-help", { &m_printHelpOnly, true } }, { "-internal", { &m_isInternal, true } }, { "-all", { &m_scanAllMode, true } }, { "-copy", { &m_copy, true } }, { "-minimal", { &m_minimal, true } }, { "-showonly", { &m_showOnly, true } }, { "-showOnly", { &m_showOnly, true } }, @@ -294,12 +371,9 @@ private: std::string *currentValue = nullptr; std::set<std::string> *currentListValue = nullptr; - for (int i = 1; i < argc; ++i) { - std::string arg(argv[i]); - if (arg.size() == 0) - continue; - if (arg.size() > 0 && arg[0] == '-') { + auto parseArgument = [¤tValue, ¤tListValue](const std::string &arg) -> bool { + if (arg[0] == '-') { currentValue = nullptr; currentListValue = nullptr; { @@ -310,7 +384,7 @@ private: return false; } currentValue = it->second.value; - continue; + return true; } } @@ -322,7 +396,7 @@ private: return false; } *(it->second.value) = true; - continue; + return true; } } @@ -334,22 +408,51 @@ private: return false; } currentListValue = it->second.value; - continue; + currentListValue->insert(""); // Indicate that argument is provided + return true; } } std::cerr << "Unknown argument: " << arg << std::endl; return false; + } + + if (currentValue != nullptr) { + *currentValue = arg; + currentValue = nullptr; + } else if (currentListValue != nullptr) { + currentListValue->insert(arg); } else { - if (currentValue != nullptr) { - *currentValue = arg; - currentValue = nullptr; - } else if (currentListValue != nullptr) { - currentListValue->insert(arg); - } else { - std::cerr << "Unknown argument: " << arg << std::endl; + std::cerr << "Unknown argument: " << arg << std::endl; + return false; + } + return true; + }; + + for (int i = 1; i < argc; ++i) { + std::string arg(argv[i]); + if (arg.empty()) + continue; + + if (arg[0] == '@') { + std::ifstream ifs(arg.substr(1), std::ifstream::in); + if (!ifs.is_open()) { + std::cerr << "Unable to open rsp file: " << arg[0] << std::endl; + return false; } + std::string argFromFile; + while (std::getline(ifs, argFromFile)) { + if (argFromFile.empty()) + continue; + if (!parseArgument(argFromFile)) + return false; + } + ifs.close(); + continue; } + + if (!parseArgument(arg)) + return false; } if (m_printHelpOnly) @@ -358,6 +461,12 @@ private: if (!qpaHeadersFilter.empty()) m_qpaHeadersRegex = std::regex(qpaHeadersFilter); + if (!rhiHeadersFilter.empty()) + m_rhiHeadersRegex = std::regex(rhiHeadersFilter); + + if (!ssgHeadersFilter.empty()) + m_ssgHeadersRegex = std::regex(ssgHeadersFilter); + if (!privateHeadersFilter.empty()) m_privateHeadersRegex = std::regex(privateHeadersFilter); @@ -365,32 +474,18 @@ private: m_publicNamespaceRegex = std::regex(publicNamespaceFilter); if (m_headers.empty() && !m_scanAllMode) { - std::cerr << "You need to specify either -headers or -all option."; + std::cerr << "You need to specify either -headers or -all option." << std::endl; return false; } if (!m_headers.empty() && m_scanAllMode) { std::cerr << "Both -headers and -all are specified. Need to choose only one" - "operational mode."; + "operational mode." << std::endl; return false; } - for (auto header : generatedHeaders) { - if (header.size() == 0) - continue; - if (header[0] == '@') { - std::ifstream ifs(header.substr(1), std::ifstream::in); - if (ifs.is_open()) { - std::string headerFromFile; - while (std::getline(ifs, headerFromFile)) { - if (!headerFromFile.empty()) - m_generatedHeaders.insert(headerFromFile); - } - } - } else { - m_generatedHeaders.insert(header); - } - } + for (const auto &argument : listArgumentMap) + argument.second.value->erase(""); bool ret = true; ret &= checkRequiredArguments(stringArgumentMap); @@ -406,7 +501,8 @@ private: { static std::array<std::string *, 8> paths = { &m_sourceDir, &m_binaryDir, &m_includeDir, &m_privateIncludeDir, - &m_qpaIncludeDir, &m_stagingDir, &m_versionScriptFile, &m_frameworkIncludeDir + &m_qpaIncludeDir, &m_rhiIncludeDir, &m_stagingDir, + &m_versionScriptFile, }; for (auto path : paths) { if (!path->empty()) @@ -420,15 +516,15 @@ private: std::string m_includeDir; std::string m_privateIncludeDir; std::string m_qpaIncludeDir; + std::string m_rhiIncludeDir; + std::string m_ssgIncludeDir; std::string m_stagingDir; std::string m_versionScriptFile; - std::string m_frameworkIncludeDir; std::set<std::string> m_knownModules; std::set<std::string> m_headers; std::set<std::string> m_generatedHeaders; bool m_scanAllMode = false; bool m_copy = false; - bool m_isFramework = false; bool m_isNonQtModule = false; bool m_isInternal = false; bool m_printHelpOnly = false; @@ -437,6 +533,8 @@ private: bool m_showOnly = false; bool m_warningsAreErrors = false; std::regex m_qpaHeadersRegex; + std::regex m_rhiHeadersRegex; + std::regex m_ssgHeadersRegex; std::regex m_privateHeadersRegex; std::regex m_publicNamespaceRegex; @@ -501,20 +599,26 @@ class SyncScanner enum { Active, Stopped, IgnoreNext, Ignore } m_versionScriptGeneratorState = Active; + std::filesystem::path m_outputRootName; std::filesystem::path m_currentFile; std::string m_currentFilename; std::string m_currentFileString; size_t m_currentFileLineNumber = 0; bool m_currentFileInSourceDir = false; - enum FileType { PublicHeader = 0, PrivateHeader = 1, QpaHeader = 2, ExportHeader = 4 }; + enum FileType { PublicHeader = 0, PrivateHeader = 1, QpaHeader = 2, ExportHeader = 4, RhiHeader = 8, SsgHeader = 16 }; unsigned int m_currentFileType = PublicHeader; int m_criticalChecks = CriticalChecks; + std::string_view m_warningMessagePreamble; public: SyncScanner(CommandLineOptions *commandLineArgs) - : m_commandLineArgs(commandLineArgs), m_masterHeaderContents(MasterHeaderIncludeComparator) + : m_commandLineArgs(commandLineArgs), + m_masterHeaderContents(MasterHeaderIncludeComparator), + m_outputRootName( + std::filesystem::weakly_canonical(m_commandLineArgs->includeDir()).root_name()), + m_warningMessagePreamble(WarningMessagePreamble) { } @@ -526,7 +630,11 @@ public: ErrorCodes sync() { - m_criticalChecks = m_commandLineArgs->warningsAreErrors() ? AllChecks : CriticalChecks; + if (m_commandLineArgs->warningsAreErrors()) { + m_criticalChecks = AllChecks; + m_warningMessagePreamble = ErrorMessagePreamble; + } + m_versionScriptGeneratorState = m_commandLineArgs->versionScriptFile().empty() ? Stopped : Active; auto error = NoError; @@ -536,13 +644,26 @@ public: if (m_commandLineArgs->scanAllMode()) { for (auto const &entry : std::filesystem::recursive_directory_iterator(m_commandLineArgs->sourceDir())) { - if (entry.is_regular_file() && isHeader(entry) - && !isDocFileHeuristic(entry.path().generic_string())) { - const std::string filePath = entry.path().generic_string(); - const std::string fileName = entry.path().filename().generic_string(); - scannerDebug() << "Checking: " << filePath << std::endl; + + const bool isRegularFile = entry.is_regular_file(); + const bool isHeaderFlag = isHeader(entry); + const bool isDocFileHeuristicFlag = + isDocFileHeuristic(entry.path().generic_string()); + const bool shouldProcessHeader = + isRegularFile && isHeaderFlag && !isDocFileHeuristicFlag; + const std::string filePath = entry.path().generic_string(); + + if (shouldProcessHeader) { + scannerDebug() << "Processing header: " << filePath << std::endl; if (!processHeader(makeHeaderAbsolute(filePath))) error = SyncFailed; + } else { + scannerDebug() + << "Skipping processing header: " << filePath + << " isRegularFile: " << isRegularFile + << " isHeaderFlag: " << isHeaderFlag + << " isDocFileHeuristicFlag: " << isDocFileHeuristicFlag + << std::endl; } } } else { @@ -552,20 +673,13 @@ public: const auto &headers = m_commandLineArgs->headers(); for (auto it = headers.begin(); it != headers.end(); ++it) { const auto &header = *it; - if (header.size() > 0 && header[0] == '@') { - std::ifstream ifs(header.substr(1), std::ifstream::in); - if (ifs.is_open()) { - std::string headerFromFile; - while (std::getline(ifs, headerFromFile)) { - if (!headerFromFile.empty()) - rspHeaders.insert(headerFromFile); - } - } - } else if (!processHeader(makeHeaderAbsolute(header))) { + scannerDebug() << "Processing header: " << header << std::endl; + if (!processHeader(makeHeaderAbsolute(header))) { error = SyncFailed; } } for (const auto &header : rspHeaders) { + scannerDebug() << "Processing header: " << header << std::endl; if (!processHeader(makeHeaderAbsolute(header))) error = SyncFailed; } @@ -634,18 +748,11 @@ public: error = SyncFailed; } - if (!m_commandLineArgs->scanAllMode()) { + if (!m_commandLineArgs->scanAllMode() && !m_commandLineArgs->stagingDir().empty()) { // Copy the generated files to a spearate staging directory to make the installation // process eaiser. if (!copyGeneratedHeadersToStagingDirectory(m_commandLineArgs->stagingDir())) error = SyncFailed; - // We also need to have a copy of the generated header files in framework include - // directories when building with '-framework'. - if (m_commandLineArgs->isFramework()) { - if (!copyGeneratedHeadersToStagingDirectory( - m_commandLineArgs->frameworkIncludeDir(), true)) - error = SyncFailed; - } } return error; } @@ -656,15 +763,37 @@ public: bool skipCleanup = false) { bool result = true; - if (!std::filesystem::exists(outputDirectory)) { - std::filesystem::create_directories(outputDirectory); - } else if (!skipCleanup) { - for (const auto &entry : - std::filesystem::recursive_directory_iterator(outputDirectory)) { - if (m_producedHeaders.find(entry.path().filename().generic_string()) - == m_producedHeaders.end()) { - std::filesystem::remove(entry.path()); + bool outDirExists = false; + if (!utils::createDirectories(outputDirectory, "Unable to create staging directory", + &outDirExists)) + return false; + + if (outDirExists && !skipCleanup) { + try { + for (const auto &entry : + std::filesystem::recursive_directory_iterator(outputDirectory)) { + if (m_producedHeaders.find(entry.path().filename().generic_string()) + == m_producedHeaders.end()) { + // Check if header file came from another module as result of the + // cross-module deprecation before removing it. + std::string firstLine; + { + std::ifstream input(entry.path(), std::ifstream::in); + if (input.is_open()) { + std::getline(input, firstLine); + input.close(); + } + } + if (firstLine.find("#ifndef DEPRECATED_HEADER_" + + m_commandLineArgs->moduleName()) + == 0 + || firstLine.find("#ifndef DEPRECATED_HEADER_") != 0) + std::filesystem::remove(entry.path()); + } } + } catch (const std::filesystem::filesystem_error &fserr) { + utils::printFilesystemError(fserr, "Unable to clean the staging directory"); + return false; } } @@ -695,6 +824,12 @@ public: if (isHeaderQpa(m_currentFilename)) m_currentFileType = QpaHeader | PrivateHeader; + if (isHeaderRhi(m_currentFilename)) + m_currentFileType = RhiHeader | PrivateHeader; + + if (isHeaderSsg(m_currentFilename)) + m_currentFileType = SsgHeader | PrivateHeader; + if (std::regex_match(m_currentFilename, ExportsHeaderRegex)) m_currentFileType |= ExportHeader; } @@ -702,7 +837,7 @@ public: [[nodiscard]] bool processHeader(const std::filesystem::path &headerFile) { // This regex filters any paths that contain the '3rdparty' directory. - static const std::regex ThirdPartyFolderRegex(".+/3rdparty/.+"); + static const std::regex ThirdPartyFolderRegex("(^|.+/)3rdparty/.+"); // This regex filters '-config.h' and '-config_p.h' header files. static const std::regex ConfigHeaderRegex("^(q|.+-)config(_p)?\\.h"); @@ -720,7 +855,7 @@ public: // Check if a directory is passed as argument. That shouldn't happen, print error and exit. if (m_currentFilename.empty()) { - std::cerr << "Header file name of " << m_currentFileString << "is empty"; + std::cerr << "Header file name of " << m_currentFileString << "is empty" << std::endl; return false; } @@ -728,27 +863,41 @@ public: FileStamp originalStamp = std::filesystem::last_write_time(headerFile, ec); if (ec) originalStamp = FileStamp::clock::now(); + ec.clear(); bool isPrivate = m_currentFileType & PrivateHeader; bool isQpa = m_currentFileType & QpaHeader; + bool isRhi = m_currentFileType & RhiHeader; + bool isSsg = m_currentFileType & SsgHeader; bool isExport = m_currentFileType & ExportHeader; - scannerDebug() << headerFile << " m_currentFilename: " << m_currentFilename - << " isPrivate: " << isPrivate << " isQpa: " << isQpa << std::endl; + scannerDebug() + << "processHeader:start: " << headerFile + << " m_currentFilename: " << m_currentFilename + << " isPrivate: " << isPrivate + << " isQpa: " << isQpa + << " isRhi: " << isRhi + << " isSsg: " << isSsg + << std::endl; // Chose the directory where to generate the header aliases or to copy header file if // the '-copy' argument is passed. std::string outputDir = m_commandLineArgs->includeDir(); if (isQpa) outputDir = m_commandLineArgs->qpaIncludeDir(); + else if (isRhi) + outputDir = m_commandLineArgs->rhiIncludeDir(); + else if (isSsg) + outputDir = m_commandLineArgs->ssgIncludeDir(); else if (isPrivate) outputDir = m_commandLineArgs->privateIncludeDir(); - if (!std::filesystem::exists(outputDir)) - std::filesystem::create_directories(outputDir); + if (!utils::createDirectories(outputDir, "Unable to create output directory")) + return false; bool headerFileExists = std::filesystem::exists(headerFile); - std::string aliasedFilepath = - std::filesystem::relative(headerFile, outputDir).generic_string(); + + std::string aliasedFilepath = headerFile.generic_string(); + std::string aliasPath = outputDir + '/' + m_currentFilename; // If the '-copy' argument is passed, we copy the original file to a corresponding output @@ -779,13 +928,20 @@ public: } bool isGenerated = isHeaderGenerated(m_currentFileString); - bool is3rdParty = std::regex_match(m_currentFileString, ThirdPartyFolderRegex); + + // Make sure that we detect the '3rdparty' directory inside the source directory only, + // since full path to the Qt sources might contain '/3rdparty/' too. + bool is3rdParty = std::regex_match( + std::filesystem::relative(headerFile, m_commandLineArgs->sourceDir()) + .generic_string(), + ThirdPartyFolderRegex); + // No processing of generated Qt config header files. if (!std::regex_match(m_currentFilename, ConfigHeaderRegex)) { unsigned int skipChecks = m_commandLineArgs->scanAllMode() ? AllChecks : NoChecks; // Collect checks that should skipped for the header file. - if (m_commandLineArgs->isNonQtModule() || is3rdParty || isQpa + if (m_commandLineArgs->isNonQtModule() || is3rdParty || isQpa || isRhi || isSsg || !m_currentFileInSourceDir || isGenerated) { skipChecks = AllChecks; } else { @@ -806,13 +962,15 @@ public: ParsingResult parsingResult; parsingResult.masterInclude = m_currentFileInSourceDir && !isExport && !is3rdParty - && !isQpa && !isPrivate && !isGenerated; - if (!parseHeader(headerFile, originalStamp, parsingResult, skipChecks)) + && !isQpa && !isRhi && !isSsg && !isPrivate && !isGenerated; + if (!parseHeader(headerFile, parsingResult, skipChecks)) { + scannerDebug() << "parseHeader failed: " << headerFile << std::endl; return false; + } // Record the private header file inside the version script content. if (isPrivate && !m_commandLineArgs->versionScriptFile().empty() - && parsingResult.versionScriptContent.size() > 0) { + && !parsingResult.versionScriptContent.empty()) { m_versionScriptContents.insert(m_versionScriptContents.end(), parsingResult.versionScriptContent.begin(), parsingResult.versionScriptContent.end()); @@ -820,12 +978,22 @@ public: // Add the '#if QT_CONFIG(<feature>)' check for header files that supposed to be // included into the module master header only if corresponding feature is enabled. - if (!isQpa && !isPrivate) { + bool willBeInModuleMasterHeader = false; + if (!isQpa && !isRhi && !isSsg && !isPrivate) { if (m_currentFilename.find('_') == std::string::npos && parsingResult.masterInclude) { m_masterHeaderContents[m_currentFilename] = parsingResult.requireConfig; + willBeInModuleMasterHeader = true; } } + + scannerDebug() + << "processHeader:end: " << headerFile + << " is3rdParty: " << is3rdParty + << " isGenerated: " << isGenerated + << " m_currentFileInSourceDir: " << m_currentFileInSourceDir + << " willBeInModuleMasterHeader: " << willBeInModuleMasterHeader + << std::endl; } else if (m_currentFilename == "qconfig.h") { // Hardcode generating of QtConfig alias updateSymbolDescriptor("QtConfig", "qconfig.h", SyncScanner::SymbolDescriptor::Pragma); @@ -849,6 +1017,7 @@ public: switch (m_versionScriptGeneratorState) { case Ignore: + scannerDebug() << "line ignored: " << buffer << std::endl; m_versionScriptGeneratorState = Active; return; case Stopped: @@ -860,6 +1029,9 @@ public: break; } + if (buffer.empty()) + return; + std::smatch match; std::string symbol; if (std::regex_match(buffer, match, VersionScriptSymbolRegex) && match[2].str().empty()) @@ -871,7 +1043,7 @@ public: symbol = match[1].str(); // checkLineForSymbols(buffer, symbol); - if (symbol.size() > 0 && symbol[symbol.size() - 1] != ';') { + if (!symbol.empty() && symbol[symbol.size() - 1] != ';') { std::string relPath = m_currentFileInSourceDir ? std::filesystem::relative(m_currentFile, m_commandLineArgs->sourceDir()) .string() @@ -910,7 +1082,7 @@ public: // 'result' the function output value that stores the result of parsing. // 'skipChecks' checks that are not applicable for the header file. [[nodiscard]] bool parseHeader(const std::filesystem::path &headerFile, - const FileStamp &timeStamp, ParsingResult &result, + ParsingResult &result, unsigned int skipChecks) { if (m_commandLineArgs->showOnly()) @@ -919,6 +1091,9 @@ public: static const std::regex MacroRegex("^\\s*#.*"); // The regex's bellow check line for known pragmas: + // + // - 'once' is not allowed in installed headers, so error out. + // // - 'qt_sync_skip_header_check' avoid any header checks. // // - 'qt_sync_stop_processing' stops the header proccesing from a moment when pragma is @@ -934,13 +1109,19 @@ public: // - 'qt_class(<symbol>)' manually declares the 'symbol' that should be used to generate // the CaMeL case header alias. // - // - 'qt_deprecates(<deprecated header file>)' indicates that this header file replaces - // the 'deprecated header file'. syncqt will create the deprecated header file' with - // the special deprecation content. See the 'generateDeprecatedHeaders' function - // for details. + // - 'qt_deprecates([module/]<deprecated header file>[,<major.minor>])' indicates that + // this header file replaces the 'deprecated header file'. syncqt will create the + // deprecated header file' with the special deprecation content. Pragma optionally + // accepts the Qt version where file should be removed. If the current Qt version is + // higher than the deprecation version, syncqt displays deprecation warning and skips + // generating the deprecated header. If the module is specified and is different from + // the one this header file belongs to, syncqt attempts to generate header files + // for the specified module. Cross-module deprecation only works within the same repo. + // See the 'generateDeprecatedHeaders' function for details. // // - 'qt_no_master_include' indicates that syncqt should avoid including this header // files into the module master header file. + static const std::regex OnceRegex(R"(^#\s*pragma\s+once$)"); static const std::regex SkipHeaderCheckRegex("^#\\s*pragma qt_sync_skip_header_check$"); static const std::regex StopProcessingRegex("^#\\s*pragma qt_sync_stop_processing$"); static const std::regex SuspendProcessingRegex("^#\\s*pragma qt_sync_suspend_processing$"); @@ -951,7 +1132,7 @@ public: // This regex checks if header contains 'We mean it' disclaimer. All private headers should // contain them. - static const std::regex WeMeantItRegex("\\s*// We mean it\\."); + static const std::string_view WeMeantItString("We mean it."); // The regex's check if the content of header files is wrapped with the Qt namespace macros. static const std::regex BeginNamespaceRegex("^QT_BEGIN_NAMESPACE(_[A-Z_]+)?$"); @@ -978,8 +1159,8 @@ public: // This regex looks for the ELFVERSION tag this is control key-word for the version script // content processing. // ELFVERSION tag accepts the following values: - // - stop - stops the symbols lookup for a versino script starting from this line. - // - ignore-next - ignores the line followed but the current one. + // - stop - stops the symbols lookup for a version script starting from this line. + // - ignore-next - ignores the line followed by the current one. // - ignore - ignores the current line. static const std::regex ElfVersionTagRegex(".*ELFVERSION:(stop|ignore-next|ignore).*"); @@ -1007,6 +1188,11 @@ public: std::size_t linesProcessed = 0; int faults = NoChecks; + const auto error = [&] () -> decltype(auto) { + return std::cerr << ErrorMessagePreamble << m_currentFileString + << ":" << m_currentFileLineNumber << " "; + }; + // Read file line by line while (std::getline(input, tmpLine)) { ++m_currentFileLineNumber; @@ -1021,6 +1207,8 @@ public: // - start-end of class/structures // And avoid processing of the the data inside these blocks. for (std::size_t i = 0; i < line.size(); ++i) { + if (line[i] == '\r') + continue; if (bracesDepth == namespaceCount) { if (line[i] == '/') { if ((i + 1) < line.size()) { @@ -1029,7 +1217,7 @@ public: continue; } else if (line[i + 1] == '/') { // Single line comment if (!(skipChecks & WeMeantItChecks) - && std::regex_match(line, WeMeantItRegex)) { + && line.find(WeMeantItString) != std::string::npos) { hasWeMeantIt = true; continue; } @@ -1053,7 +1241,8 @@ public: } if (isMultiLineComment) { - if (!(skipChecks & WeMeantItChecks) && std::regex_match(line, WeMeantItRegex)) { + if (!(skipChecks & WeMeantItChecks) && + line.find(WeMeantItString) != std::string::npos) { hasWeMeantIt = true; continue; } @@ -1080,18 +1269,20 @@ public: } line.clear(); - if (buffer.size() == 0) - continue; scannerDebug() << m_currentFilename << ": " << buffer << std::endl; if (m_currentFileType & PrivateHeader) { parseVersionScriptContent(buffer, result); } + if (buffer.empty()) + continue; + ++linesProcessed; bool skipSymbols = - (m_currentFileType & PrivateHeader) || (m_currentFileType & QpaHeader); + (m_currentFileType & PrivateHeader) || (m_currentFileType & QpaHeader) || (m_currentFileType & RhiHeader) + || (m_currentFileType & SsgHeader); // Parse pragmas if (std::regex_match(buffer, MacroRegex)) { @@ -1118,6 +1309,13 @@ public: } else if (std::regex_match(buffer, match, DeprecatesPragmaRegex)) { m_deprecatedHeaders[match[1].str()] = m_commandLineArgs->moduleName() + '/' + m_currentFilename; + } else if (std::regex_match(buffer, OnceRegex)) { + if (!(skipChecks & PragmaOnceChecks)) { + faults |= PragmaOnceChecks; + error() << "\"#pragma once\" is not allowed in installed header files: " + "https://lists.qt-project.org/pipermail/development/2022-October/043121.html" + << std::endl; + } } else if (std::regex_match(buffer, match, IncludeRegex) && !isSuspended) { if (!(skipChecks & IncludeChecks)) { std::string includedHeader = match[1].str(); @@ -1126,16 +1324,14 @@ public: .filename() .generic_string())) { faults |= PrivateHeaderChecks; - std::cerr << "ERROR: " << m_currentFileString - << ":" << m_currentFileLineNumber - << " includes private header " << includedHeader << std::endl; + error() << "includes private header " << includedHeader << std::endl; } for (const auto &module : m_commandLineArgs->knownModules()) { std::string suggestedHeader = "Qt" + module + '/' + includedHeader; if (std::filesystem::exists(m_commandLineArgs->includeDir() + "/../" + suggestedHeader)) { faults |= IncludeChecks; - std::cerr << "WARNING: " << m_currentFileString + std::cerr << m_warningMessagePreamble << m_currentFileString << ":" << m_currentFileLineNumber << " includes " << includedHeader << " when it should include " @@ -1187,27 +1383,28 @@ public: } } } + input.close(); // Error out if namespace checks are failed. if (!(skipChecks & NamespaceChecks)) { if (hasQtBeginNamespace) { if (qtBeginNamespace != qtEndNamespace) { faults |= NamespaceChecks; - std::cerr << "WARNING: " << m_currentFileString + std::cerr << m_warningMessagePreamble << m_currentFileString << " the begin namespace macro QT_BEGIN_NAMESPACE" << qtBeginNamespace << " doesn't match the end namespace macro QT_END_NAMESPACE" << qtEndNamespace << std::endl; } } else { faults |= NamespaceChecks; - std::cerr << "WARNING: " << m_currentFileString + std::cerr << m_warningMessagePreamble << m_currentFileString << " does not include QT_BEGIN_NAMESPACE" << std::endl; } } if (!(skipChecks & WeMeantItChecks) && !hasWeMeantIt) { faults |= WeMeantItChecks; - std::cerr << "WARNING: " << m_currentFileString + std::cerr << m_warningMessagePreamble << m_currentFileString << " does not have the \"We mean it.\" warning" << std::endl; } @@ -1272,6 +1469,16 @@ public: return std::regex_match(headerFileName, m_commandLineArgs->qpaHeadersRegex()); } + [[nodiscard]] bool isHeaderRhi(const std::string &headerFileName) + { + return std::regex_match(headerFileName, m_commandLineArgs->rhiHeadersRegex()); + } + + [[nodiscard]] bool isHeaderSsg(const std::string &headerFileName) + { + return std::regex_match(headerFileName, m_commandLineArgs->ssgHeadersRegex()); + } + [[nodiscard]] bool isHeaderPrivate(const std::string &headerFile) { return std::regex_match(headerFile, m_commandLineArgs->privateHeadersRegex()); @@ -1359,13 +1566,53 @@ public: { static std::regex cIdentifierSymbolsRegex("[^a-zA-Z0-9_]"); static std::string guard_base = "DEPRECATED_HEADER_" + m_commandLineArgs->moduleName(); + bool result = true; for (auto it = m_deprecatedHeaders.begin(); it != m_deprecatedHeaders.end(); ++it) { - std::string &replacement = it->second; + const std::string &descriptor = it->first; + const std::string &replacement = it->second; + + const auto separatorPos = descriptor.find(','); + std::string headerPath = descriptor.substr(0, separatorPos); + std::string versionDisclaimer; + if (separatorPos != std::string::npos) { + std::string version = descriptor.substr(separatorPos + 1); + versionDisclaimer = " and will be removed in Qt " + version; + int minor = 0; + int major = 0; + if (!utils::parseVersion(version, major, minor)) { + std::cerr << ErrorMessagePreamble + << "Invalid version format specified for the deprecated header file " + << headerPath << ": '" << version + << "'. Expected format: 'major.minor'.\n"; + result = false; + continue; + } + + if (QT_VERSION_MAJOR > major + || (QT_VERSION_MAJOR == major && QT_VERSION_MINOR >= minor)) { + std::cerr << WarningMessagePreamble << headerPath + << " is marked as deprecated and will not be generated in Qt " + << QT_VERSION_STR + << ". The respective qt_deprecates pragma needs to be removed.\n"; + continue; + } + } + + const auto moduleSeparatorPos = headerPath.find('/'); + std::string headerName = moduleSeparatorPos != std::string::npos + ? headerPath.substr(moduleSeparatorPos + 1) + : headerPath; + const std::string moduleName = moduleSeparatorPos != std::string::npos + ? headerPath.substr(0, moduleSeparatorPos) + : m_commandLineArgs->moduleName(); + + bool isCrossModuleDeprecation = moduleName != m_commandLineArgs->moduleName(); + std::string qualifiedHeaderName = - std::regex_replace(it->first, cIdentifierSymbolsRegex, "_"); + std::regex_replace(headerName, cIdentifierSymbolsRegex, "_"); std::string guard = guard_base + "_" + qualifiedHeaderName; - std::string warningText = "Header <" + m_commandLineArgs->moduleName() + "/" + it->first - + "> is deprecated. Please include <" + replacement + "> instead."; + std::string warningText = "Header <" + moduleName + "/" + headerName + "> is deprecated" + + versionDisclaimer + ". Please include <" + replacement + "> instead."; std::stringstream buffer; buffer << "#ifndef " << guard << "\n" << "#define " << guard << "\n" @@ -1375,15 +1622,21 @@ public: << "# pragma message (\"" << warningText << "\")\n" << "#endif\n" << "#include <" << replacement << ">\n" - << "#if 0\n" - // TODO: Looks like qt_no_master_include is useless since deprecated headers are - // generated by syncqt but are never scanned. - << "#pragma qt_no_master_include\n" - << "#endif\n" << "#endif\n"; - writeIfDifferent(m_commandLineArgs->includeDir() + '/' + it->first, buffer.str()); + + const std::string outputDir = isCrossModuleDeprecation + ? m_commandLineArgs->includeDir() + "/../" + moduleName + : m_commandLineArgs->includeDir(); + writeIfDifferent(outputDir + '/' + headerName, buffer.str()); + + // Add header file to staging installation directory for cross-module deprecation case. + if (isCrossModuleDeprecation) { + const std::string stagingDir = outputDir + "/.syncqt_staging/"; + writeIfDifferent(stagingDir + headerName, buffer.str()); + } + m_producedHeaders.insert(headerName); } - return true; + return result; } [[nodiscard]] bool generateHeaderCheckExceptions() @@ -1405,7 +1658,7 @@ public: return writeIfDifferent(m_commandLineArgs->versionScriptFile(), buffer.str()); } - bool updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst); + bool updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst) noexcept; void updateSymbolDescriptor(const std::string &symbol, const std::string &file, SymbolDescriptor::SourceType type); }; @@ -1431,7 +1684,8 @@ SyncScanner::makeHeaderAbsolute(const std::string &filename) const return utils::normilizedPath(filename); } -bool SyncScanner::updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst) +bool SyncScanner::updateOrCopy(const std::filesystem::path &src, + const std::filesystem::path &dst) noexcept { if (m_commandLineArgs->showOnly()) return true; @@ -1478,7 +1732,7 @@ bool SyncScanner::generateQtCamelCaseFileIfContentChanged(const std::string &out std::string buffer = "#include \""; buffer += aliasedFilePath; - buffer += "\"\n"; + buffer += "\" // IWYU pragma: export\n"; return writeIfDifferent(outputFilePath, buffer); } @@ -1505,7 +1759,7 @@ bool SyncScanner::generateAliasedHeaderFileIfTimestampChanged(const std::string std::cerr << "Unable to write header file alias: " << outputFilePath << std::endl; return false; } - ofs << "#include \"" << aliasedFilePath << "\"\n"; + ofs << "#include \"" << aliasedFilePath << "\" // IWYU pragma: export\n"; ofs.close(); return true; } @@ -1520,15 +1774,26 @@ bool SyncScanner::writeIfDifferent(const std::string &outputFile, const std::str std::filesystem::path outputFilePath(outputFile); std::string outputDirectory = outputFilePath.parent_path().string(); - if (!std::filesystem::exists(outputDirectory)) - std::filesystem::create_directories(outputDirectory); + + if (!utils::createDirectories(outputDirectory, "Unable to create output directory")) + return false; + + auto expectedSize = buffer.size(); +#ifdef _WINDOWS + // File on disk has \r\n instead of just \n + expectedSize += std::count(buffer.begin(), buffer.end(), '\n'); +#endif if (std::filesystem::exists(outputFilePath) - && buffer.size() == std::filesystem::file_size(outputFilePath)) { + && expectedSize == std::filesystem::file_size(outputFilePath)) { char rdBuffer[bufferSize]; memset(rdBuffer, 0, bufferSize); std::ifstream ifs(outputFile, std::fstream::in); + if (!ifs.is_open()) { + std::cerr << "Unable to open " << outputFile << " for comparison." << std::endl; + return false; + } std::streamsize currentPos = 0; std::size_t bytesRead = 0; |