syncqt uses qt_deprecates pragma to generate deprecated header files that might live forever in Qt packages. This adds the version argument to the pragma, that allows specifying the version when the respective header file should be removed. The new pragma format is the following: pragma qt_deprecates(<header>[,<major.minor>]) If deprecation version of the deprecated header file is lower than the current Qt version, syncqt will display the respective warning in the log and skip generating the deprectated header file. Also the user warning message now displays the exact version when the header file will be removed. Task-number: QTBUG-115029 Change-Id: Ifd7464b8539b8be93b95690fd1ca70ef0b14e436 Reviewed-by: Alexandru Croitor <alexandru.croitor@qt.io> Reviewed-by: Edward Welbourne <edward.welbourne@qt.io> Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io> (cherry picked from commit 4bc0a08bea3c262ebda26163f9271c946c430ff0) Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
1734 lines
74 KiB
C++
1734 lines
74 KiB
C++
// Copyright (C) 2022 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
/*
|
|
* The tool generates deployment artifacts for the Qt builds such as:
|
|
* - CaMeL case header files named by public C++ symbols located in public module header files
|
|
* - 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.
|
|
* 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
|
|
* on the selected mode, the tool either scans the filesystem to find header files or use the
|
|
* pre-defined list of header files.
|
|
*/
|
|
|
|
#include <iostream>
|
|
#include <fstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <cstring>
|
|
#include <sstream>
|
|
#include <filesystem>
|
|
#include <unordered_map>
|
|
#include <vector>
|
|
#include <regex>
|
|
#include <map>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <array>
|
|
|
|
enum ErrorCodes {
|
|
NoError = 0,
|
|
InvalidArguments,
|
|
SyncFailed,
|
|
};
|
|
|
|
// Enum contains the list of checks that can be executed on header files.
|
|
enum HeaderChecks {
|
|
NoChecks = 0,
|
|
NamespaceChecks = 1, /* Checks if header file is wrapped with QT_<BEGIN|END>_NAMESPACE macros */
|
|
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,
|
|
};
|
|
|
|
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)
|
|
{
|
|
std::smatch amatch;
|
|
std::smatch bmatch;
|
|
|
|
if (std::regex_match(a, amatch, GlobalHeaderRegex)) {
|
|
if (std::regex_match(b, bmatch, GlobalHeaderRegex)) {
|
|
return amatch[1].str().empty()
|
|
|| (!bmatch[1].str().empty() && amatch[1].str() < bmatch[1].str());
|
|
}
|
|
return true;
|
|
} else if (std::regex_match(b, bmatch, GlobalHeaderRegex)) {
|
|
return false;
|
|
}
|
|
|
|
return a < b;
|
|
};
|
|
|
|
namespace utils {
|
|
std::string asciiToLower(std::string s)
|
|
{
|
|
std::transform(s.begin(), s.end(), s.begin(),
|
|
[](unsigned char c) { return (c >= 'A' && c <= 'Z') ? c | 0x20 : c; });
|
|
return s;
|
|
}
|
|
|
|
std::string asciiToUpper(std::string s)
|
|
{
|
|
std::transform(s.begin(), s.end(), s.begin(),
|
|
[](unsigned char c) { return (c >= 'a' && c <= 'z') ? c & 0xdf : c; });
|
|
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
|
|
{
|
|
int overflow(int c) override { return c; }
|
|
} buff;
|
|
|
|
public:
|
|
DummyOutputStream() : std::ostream(&buff) { }
|
|
} DummyOutput;
|
|
|
|
void printInternalError()
|
|
{
|
|
std::cerr << "Internal error. Please create bugreport at https://bugreports.qt.io "
|
|
"using 'Build tools: Other component.'"
|
|
<< std::endl;
|
|
}
|
|
|
|
std::filesystem::path normilizedPath(const std::string &path)
|
|
{
|
|
return std::filesystem::path(std::filesystem::weakly_canonical(path).generic_string());
|
|
}
|
|
}
|
|
|
|
using FileStamp = std::filesystem::file_time_type;
|
|
|
|
class CommandLineOptions
|
|
{
|
|
template<typename T>
|
|
struct CommandLineOption
|
|
{
|
|
CommandLineOption(T *_value, bool _isOptional = false)
|
|
: value(_value), isOptional(_isOptional)
|
|
{
|
|
}
|
|
|
|
T *value;
|
|
bool isOptional;
|
|
};
|
|
|
|
public:
|
|
CommandLineOptions(int argc, char *argv[]) : m_isValid(parseArguments(argc, argv)) { }
|
|
|
|
bool isValid() const { return m_isValid; }
|
|
|
|
const std::string &moduleName() const { return m_moduleName; }
|
|
|
|
const std::string &sourceDir() const { return m_sourceDir; }
|
|
|
|
const std::string &binaryDir() const { return m_binaryDir; }
|
|
|
|
const std::string &includeDir() const { return m_includeDir; }
|
|
|
|
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 &stagingDir() const { return m_stagingDir; }
|
|
|
|
const std::string &versionScriptFile() const { return m_versionScriptFile; }
|
|
|
|
const std::set<std::string> &knownModules() const { return m_knownModules; }
|
|
|
|
const std::regex &qpaHeadersRegex() const { return m_qpaHeadersRegex; }
|
|
|
|
const std::regex &rhiHeadersRegex() const { return m_rhiHeadersRegex; }
|
|
|
|
const std::regex &privateHeadersRegex() const { return m_privateHeadersRegex; }
|
|
|
|
const std::regex &publicNamespaceRegex() const { return m_publicNamespaceRegex; }
|
|
|
|
const std::set<std::string> &headers() const { return m_headers; }
|
|
|
|
const std::set<std::string> &generatedHeaders() const { return m_generatedHeaders; }
|
|
|
|
bool scanAllMode() const { return m_scanAllMode; }
|
|
|
|
bool isFramework() const { return m_isFramework; }
|
|
|
|
bool isInternal() const { return m_isInternal; }
|
|
|
|
bool isNonQtModule() const { return m_isNonQtModule; }
|
|
|
|
bool printHelpOnly() const { return m_printHelpOnly; }
|
|
|
|
bool debug() const { return m_debug; }
|
|
|
|
bool copy() const { return m_copy; }
|
|
|
|
bool minimal() const { return m_minimal; }
|
|
|
|
bool showOnly() const { return m_showOnly; }
|
|
|
|
bool warningsAreErrors() const { return m_warningsAreErrors; }
|
|
|
|
void printHelp() const
|
|
{
|
|
std::cout << "Usage: syncqt -sourceDir <dir> -binaryDir <dir> -module <module name>"
|
|
" -includeDir <dir> -privateIncludeDir <dir> -qpaIncludeDir <dir> -rhiIncludeDir <dir>"
|
|
" -stagingDir <dir> <-headers <header list>|-all> [-debug]"
|
|
" [-versionScript <path>] [-qpaHeadersFilter <regex>] [-rhiHeadersFilter <regex>]"
|
|
" [-framework [-frameworkIncludeDir <dir>]]"
|
|
" [-knownModules <module1> <module2>... <moduleN>]"
|
|
" [-nonQt] [-internal] [-copy]\n"
|
|
""
|
|
"Mandatory arguments:\n"
|
|
" -module Module name.\n"
|
|
" -headers List of header files.\n"
|
|
" -all In 'all' mode syncqt scans source\n"
|
|
" directory for public qt headers and\n"
|
|
" artifacts not considering CMake source\n"
|
|
" tree. The main use cases are the \n"
|
|
" generating of documentation and creating\n"
|
|
" API review changes.\n"
|
|
" -sourceDir Module source directory.\n"
|
|
" -binaryDir Module build directory.\n"
|
|
" -includeDir Module include directory where the\n"
|
|
" generated header files will be located.\n"
|
|
" -privateIncludeDir Module include directory for the\n"
|
|
" 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"
|
|
" -stagingDir Temporary staging directory to collect\n"
|
|
" artifacts that need to be installed.\n"
|
|
" -knownModules list of known modules. syncqt uses the\n"
|
|
" list to check the #include macros\n"
|
|
" consistency.\n"
|
|
"Optional arguments:\n"
|
|
" -internal Indicates that the module is internal.\n"
|
|
" -nonQt Indicates that the module is not a Qt\n"
|
|
" module.\n"
|
|
" -privateHeadersFilter Regex that filters private header files\n"
|
|
" 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"
|
|
" -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"
|
|
" public C++ symbols.\n"
|
|
" -showonly Show actions, but not perform them.\n"
|
|
" -warningsAreErrors Treat all warnings as errors.\n"
|
|
" -help Print this help.\n";
|
|
}
|
|
|
|
private:
|
|
template<typename T>
|
|
[[nodiscard]] bool checkRequiredArguments(const std::unordered_map<std::string, T> &arguments)
|
|
{
|
|
bool ret = true;
|
|
for (const auto &argument : arguments) {
|
|
if (!argument.second.isOptional
|
|
&& (!argument.second.value || argument.second.value->size()) == 0) {
|
|
std::cerr << "Missing argument: " << argument.first << std::endl;
|
|
ret = false;
|
|
}
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
[[nodiscard]] bool parseArguments(int argc, char *argv[])
|
|
{
|
|
std::string qpaHeadersFilter;
|
|
std::string rhiHeadersFilter;
|
|
std::string privateHeadersFilter;
|
|
std::string publicNamespaceFilter;
|
|
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 } },
|
|
{ "-includeDir", { &m_includeDir } },
|
|
{ "-privateIncludeDir", { &m_privateIncludeDir } },
|
|
{ "-qpaIncludeDir", { &m_qpaIncludeDir } },
|
|
{ "-rhiIncludeDir", { &m_rhiIncludeDir } },
|
|
{ "-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", { &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 } },
|
|
{ "-internal", { &m_isInternal, true } }, { "-all", { &m_scanAllMode, true } },
|
|
{ "-copy", { &m_copy, true } }, { "-minimal", { &m_minimal, true } },
|
|
{ "-showonly", { &m_showOnly, true } }, { "-showOnly", { &m_showOnly, true } },
|
|
{ "-warningsAreErrors", { &m_warningsAreErrors, true } }
|
|
};
|
|
|
|
std::string *currentValue = nullptr;
|
|
std::set<std::string> *currentListValue = nullptr;
|
|
|
|
auto parseArgument = [¤tValue, ¤tListValue](const std::string &arg) -> bool {
|
|
if (arg[0] == '-') {
|
|
currentValue = nullptr;
|
|
currentListValue = nullptr;
|
|
{
|
|
auto it = stringArgumentMap.find(arg);
|
|
if (it != stringArgumentMap.end()) {
|
|
if (it->second.value == nullptr) {
|
|
utils::printInternalError();
|
|
return false;
|
|
}
|
|
currentValue = it->second.value;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
{
|
|
auto it = boolArgumentMap.find(arg);
|
|
if (it != boolArgumentMap.end()) {
|
|
if (it->second.value == nullptr) {
|
|
utils::printInternalError();
|
|
return false;
|
|
}
|
|
*(it->second.value) = true;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
{
|
|
auto it = listArgumentMap.find(arg);
|
|
if (it != listArgumentMap.end()) {
|
|
if (it->second.value == nullptr) {
|
|
utils::printInternalError();
|
|
return false;
|
|
}
|
|
currentListValue = it->second.value;
|
|
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 {
|
|
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;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (!parseArgument(arg))
|
|
return false;
|
|
}
|
|
|
|
if (m_printHelpOnly)
|
|
return true;
|
|
|
|
if (!qpaHeadersFilter.empty())
|
|
m_qpaHeadersRegex = std::regex(qpaHeadersFilter);
|
|
|
|
if (!rhiHeadersFilter.empty())
|
|
m_rhiHeadersRegex = std::regex(rhiHeadersFilter);
|
|
|
|
if (!privateHeadersFilter.empty())
|
|
m_privateHeadersRegex = std::regex(privateHeadersFilter);
|
|
|
|
if (!publicNamespaceFilter.empty())
|
|
m_publicNamespaceRegex = std::regex(publicNamespaceFilter);
|
|
|
|
if (m_headers.empty() && !m_scanAllMode) {
|
|
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." << std::endl;
|
|
return false;
|
|
}
|
|
|
|
for (const auto &argument : listArgumentMap)
|
|
argument.second.value->erase("");
|
|
|
|
bool ret = true;
|
|
ret &= checkRequiredArguments(stringArgumentMap);
|
|
ret &= checkRequiredArguments(listArgumentMap);
|
|
|
|
normilizePaths();
|
|
|
|
return ret;
|
|
}
|
|
|
|
// Convert all paths from command line to a generic one.
|
|
void normilizePaths()
|
|
{
|
|
static std::array<std::string *, 9> paths = {
|
|
&m_sourceDir, &m_binaryDir, &m_includeDir, &m_privateIncludeDir,
|
|
&m_qpaIncludeDir, &m_rhiIncludeDir, &m_stagingDir,
|
|
&m_versionScriptFile, &m_frameworkIncludeDir
|
|
};
|
|
for (auto path : paths) {
|
|
if (!path->empty())
|
|
*path = utils::normilizedPath(*path).generic_string();
|
|
}
|
|
}
|
|
|
|
std::string m_moduleName;
|
|
std::string m_sourceDir;
|
|
std::string m_binaryDir;
|
|
std::string m_includeDir;
|
|
std::string m_privateIncludeDir;
|
|
std::string m_qpaIncludeDir;
|
|
std::string m_rhiIncludeDir;
|
|
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;
|
|
bool m_debug = false;
|
|
bool m_minimal = false;
|
|
bool m_showOnly = false;
|
|
bool m_warningsAreErrors = false;
|
|
std::regex m_qpaHeadersRegex;
|
|
std::regex m_rhiHeadersRegex;
|
|
std::regex m_privateHeadersRegex;
|
|
std::regex m_publicNamespaceRegex;
|
|
|
|
bool m_isValid;
|
|
};
|
|
|
|
class SyncScanner
|
|
{
|
|
class SymbolDescriptor
|
|
{
|
|
public:
|
|
// Where the symbol comes from
|
|
enum SourceType {
|
|
Pragma = 0, // pragma qt_class is mentioned a header file
|
|
Declaration, // The symbol declaration inside a header file
|
|
MaxSourceType
|
|
};
|
|
|
|
void update(const std::string &file, SourceType type)
|
|
{
|
|
if (type < m_type) {
|
|
m_file = file;
|
|
m_type = type;
|
|
}
|
|
}
|
|
|
|
// The file that contains a symbol.
|
|
const std::string &file() const { return m_file; }
|
|
|
|
private:
|
|
SourceType m_type = MaxSourceType;
|
|
std::string m_file;
|
|
};
|
|
using SymbolContainer = std::unordered_map<std::string, SymbolDescriptor>;
|
|
|
|
struct ParsingResult
|
|
{
|
|
std::vector<std::string> versionScriptContent;
|
|
std::string requireConfig;
|
|
bool masterInclude = true;
|
|
};
|
|
|
|
CommandLineOptions *m_commandLineArgs = nullptr;
|
|
|
|
std::map<std::string /* header file name */, std::string /* header feature guard name */,
|
|
decltype(MasterHeaderIncludeComparator) *>
|
|
m_masterHeaderContents;
|
|
|
|
std::unordered_map<std::string /* the deprecated header name*/,
|
|
std::string /* the replacement */>
|
|
m_deprecatedHeaders;
|
|
std::vector<std::string> m_versionScriptContents;
|
|
std::set<std::string> m_producedHeaders;
|
|
std::vector<std::string> m_headerCheckExceptions;
|
|
SymbolContainer m_symbols;
|
|
std::ostream &scannerDebug() const
|
|
{
|
|
if (m_commandLineArgs->debug())
|
|
return std::cout;
|
|
return utils::DummyOutput;
|
|
}
|
|
|
|
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, RhiHeader = 8 };
|
|
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_outputRootName(
|
|
std::filesystem::weakly_canonical(m_commandLineArgs->includeDir()).root_name()),
|
|
m_warningMessagePreamble(WarningMessagePreamble)
|
|
{
|
|
}
|
|
|
|
// The function converts the relative path to a header files to the absolute. It also makes the
|
|
// path canonical(removes '..' and '.' parts of the path). The source directory passed in
|
|
// '-sourceDir' command line argument is used as base path for relative paths to create the
|
|
// absolute path.
|
|
[[nodiscard]] std::filesystem::path makeHeaderAbsolute(const std::string &filename) const;
|
|
|
|
ErrorCodes sync()
|
|
{
|
|
if (m_commandLineArgs->warningsAreErrors()) {
|
|
m_criticalChecks = AllChecks;
|
|
m_warningMessagePreamble = ErrorMessagePreamble;
|
|
}
|
|
|
|
m_versionScriptGeneratorState =
|
|
m_commandLineArgs->versionScriptFile().empty() ? Stopped : Active;
|
|
auto error = NoError;
|
|
|
|
// In the scan all mode we ingore the list of header files that is specified in the
|
|
// '-headers' argument, and collect header files from the source directory tree.
|
|
if (m_commandLineArgs->scanAllMode()) {
|
|
for (auto const &entry :
|
|
std::filesystem::recursive_directory_iterator(m_commandLineArgs->sourceDir())) {
|
|
|
|
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 {
|
|
// Since the list of header file is quite big syncqt supports response files to avoid
|
|
// the issues with long command lines.
|
|
std::set<std::string> rspHeaders;
|
|
const auto &headers = m_commandLineArgs->headers();
|
|
for (auto it = headers.begin(); it != headers.end(); ++it) {
|
|
const auto &header = *it;
|
|
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;
|
|
}
|
|
}
|
|
|
|
// No further processing in minimal mode.
|
|
if (m_commandLineArgs->minimal())
|
|
return error;
|
|
|
|
// Generate aliases for all unique symbols collected during the header files parsing.
|
|
for (auto it = m_symbols.begin(); it != m_symbols.end(); ++it) {
|
|
const std::string &filename = it->second.file();
|
|
if (!filename.empty()) {
|
|
if (generateQtCamelCaseFileIfContentChanged(
|
|
m_commandLineArgs->includeDir() + '/' + it->first, filename)) {
|
|
m_producedHeaders.insert(it->first);
|
|
} else {
|
|
error = SyncFailed;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Generate the header file containing version information.
|
|
if (!m_commandLineArgs->isNonQtModule()) {
|
|
std::string moduleNameLower = utils::asciiToLower(m_commandLineArgs->moduleName());
|
|
std::string versionHeaderFilename(moduleNameLower + "version.h");
|
|
std::string versionHeaderCamel(m_commandLineArgs->moduleName() + "Version");
|
|
std::string versionFile = m_commandLineArgs->includeDir() + '/' + versionHeaderFilename;
|
|
|
|
std::error_code ec;
|
|
FileStamp originalStamp = std::filesystem::last_write_time(versionFile, ec);
|
|
if (ec)
|
|
originalStamp = FileStamp::clock::now();
|
|
|
|
if (generateVersionHeader(versionFile)) {
|
|
if (!generateAliasedHeaderFileIfTimestampChanged(
|
|
m_commandLineArgs->includeDir() + '/' + versionHeaderCamel,
|
|
versionHeaderFilename, originalStamp)) {
|
|
error = SyncFailed;
|
|
}
|
|
m_masterHeaderContents[versionHeaderFilename] = {};
|
|
m_producedHeaders.insert(versionHeaderFilename);
|
|
m_producedHeaders.insert(versionHeaderCamel);
|
|
} else {
|
|
error = SyncFailed;
|
|
}
|
|
}
|
|
|
|
if (!m_commandLineArgs->scanAllMode()) {
|
|
if (!m_commandLineArgs->isNonQtModule()) {
|
|
if (!generateDeprecatedHeaders())
|
|
error = SyncFailed;
|
|
|
|
if (!generateHeaderCheckExceptions())
|
|
error = SyncFailed;
|
|
}
|
|
|
|
if (!m_commandLineArgs->versionScriptFile().empty()) {
|
|
if (!generateLinkerVersionScript())
|
|
error = SyncFailed;
|
|
}
|
|
}
|
|
|
|
if (!m_commandLineArgs->isNonQtModule()) {
|
|
if (!generateMasterHeader())
|
|
error = SyncFailed;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// The function copies files, that were generated while the sync procedure to a staging
|
|
// directory. This is necessary to simplify the installation of the generated files.
|
|
[[nodiscard]] bool copyGeneratedHeadersToStagingDirectory(const std::string &outputDirectory,
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto &header : m_producedHeaders) {
|
|
std::filesystem::path src(m_commandLineArgs->includeDir() + '/' + header);
|
|
std::filesystem::path dst(outputDirectory + '/' + header);
|
|
if (!m_commandLineArgs->showOnly())
|
|
result &= updateOrCopy(src, dst);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void resetCurrentFileInfoData(const std::filesystem::path &headerFile)
|
|
{
|
|
// This regex filters the generated '*exports.h' and '*exports_p.h' header files.
|
|
static const std::regex ExportsHeaderRegex("^q(.*)exports(_p)?\\.h$");
|
|
|
|
m_currentFile = headerFile;
|
|
m_currentFileLineNumber = 0;
|
|
m_currentFilename = m_currentFile.filename().generic_string();
|
|
m_currentFileType = PublicHeader;
|
|
m_currentFileString = m_currentFile.generic_string();
|
|
m_currentFileInSourceDir = m_currentFileString.find(m_commandLineArgs->sourceDir()) == 0;
|
|
|
|
if (isHeaderPrivate(m_currentFilename))
|
|
m_currentFileType = PrivateHeader;
|
|
|
|
if (isHeaderQpa(m_currentFilename))
|
|
m_currentFileType = QpaHeader | PrivateHeader;
|
|
|
|
if (isHeaderRhi(m_currentFilename))
|
|
m_currentFileType = RhiHeader | PrivateHeader;
|
|
|
|
if (std::regex_match(m_currentFilename, ExportsHeaderRegex))
|
|
m_currentFileType |= ExportHeader;
|
|
}
|
|
|
|
[[nodiscard]] bool processHeader(const std::filesystem::path &headerFile)
|
|
{
|
|
// This regex filters any paths that contain the '3rdparty' directory.
|
|
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");
|
|
|
|
resetCurrentFileInfoData(headerFile);
|
|
// We assume that header files ouside of the module source or build directories do not
|
|
// belong to the module. Skip any processing.
|
|
if (!m_currentFileInSourceDir
|
|
&& m_currentFileString.find(m_commandLineArgs->binaryDir()) != 0) {
|
|
scannerDebug() << "Header file: " << headerFile
|
|
<< " is outside the sync directories. Skipping." << std::endl;
|
|
m_headerCheckExceptions.push_back(m_currentFileString);
|
|
return true;
|
|
}
|
|
|
|
// 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::endl;
|
|
return false;
|
|
}
|
|
|
|
std::error_code ec;
|
|
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 isExport = m_currentFileType & ExportHeader;
|
|
scannerDebug()
|
|
<< "processHeader:start: " << headerFile
|
|
<< " m_currentFilename: " << m_currentFilename
|
|
<< " isPrivate: " << isPrivate
|
|
<< " isQpa: " << isQpa
|
|
<< " isRhi: " << isRhi
|
|
<< 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 (isPrivate)
|
|
outputDir = m_commandLineArgs->privateIncludeDir();
|
|
|
|
if (!std::filesystem::exists(outputDir))
|
|
std::filesystem::create_directories(outputDir);
|
|
|
|
bool headerFileExists = std::filesystem::exists(headerFile);
|
|
|
|
std::filesystem::path headerFileRootName =
|
|
std::filesystem::weakly_canonical(headerFile, ec).root_name();
|
|
std::string aliasedFilepath = !ec && headerFileRootName == m_outputRootName
|
|
? std::filesystem::relative(headerFile, outputDir).generic_string()
|
|
: headerFile.generic_string();
|
|
ec.clear();
|
|
|
|
std::string aliasPath = outputDir + '/' + m_currentFilename;
|
|
|
|
// If the '-copy' argument is passed, we copy the original file to a corresponding output
|
|
// directory otherwise we only create a header file alias that contains relative path to
|
|
// the original header file in the module source or build tree.
|
|
if (m_commandLineArgs->copy() && headerFileExists) {
|
|
if (!updateOrCopy(headerFile, aliasPath))
|
|
return false;
|
|
} else {
|
|
if (!generateAliasedHeaderFileIfTimestampChanged(aliasPath, aliasedFilepath,
|
|
originalStamp))
|
|
return false;
|
|
}
|
|
|
|
// No further processing in minimal mode.
|
|
if (m_commandLineArgs->minimal())
|
|
return true;
|
|
|
|
// Stop processing if header files doesn't exist. This happens at configure time, since
|
|
// either header files are generated later than syncqt is running or header files only
|
|
// generated at build time. These files will be processed at build time, if CMake files
|
|
// contain the correct dependencies between the missing header files and the module
|
|
// 'sync_headers' targets.
|
|
if (!headerFileExists) {
|
|
scannerDebug() << "Header file: " << headerFile
|
|
<< " doesn't exist, but is added to syncqt scanning. Skipping.";
|
|
return true;
|
|
}
|
|
|
|
bool isGenerated = isHeaderGenerated(m_currentFileString);
|
|
bool is3rdParty = std::regex_match(m_currentFileString, 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 || isRhi
|
|
|| !m_currentFileInSourceDir || isGenerated) {
|
|
skipChecks = AllChecks;
|
|
} else {
|
|
if (std::regex_match(m_currentFilename, GlobalHeaderRegex) || isExport)
|
|
skipChecks |= NamespaceChecks;
|
|
|
|
if (isHeaderPCH(m_currentFilename))
|
|
skipChecks |= WeMeantItChecks;
|
|
|
|
if (isPrivate) {
|
|
skipChecks |= NamespaceChecks;
|
|
skipChecks |= PrivateHeaderChecks;
|
|
skipChecks |= IncludeChecks;
|
|
} else {
|
|
skipChecks |= WeMeantItChecks;
|
|
}
|
|
}
|
|
|
|
ParsingResult parsingResult;
|
|
parsingResult.masterInclude = m_currentFileInSourceDir && !isExport && !is3rdParty
|
|
&& !isQpa && !isRhi && !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.empty()) {
|
|
m_versionScriptContents.insert(m_versionScriptContents.end(),
|
|
parsingResult.versionScriptContent.begin(),
|
|
parsingResult.versionScriptContent.end());
|
|
}
|
|
|
|
// 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.
|
|
bool willBeInModuleMasterHeader = false;
|
|
if (!isQpa && !isRhi && !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);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void parseVersionScriptContent(const std::string buffer, ParsingResult &result)
|
|
{
|
|
// This regex looks for the symbols that needs to be placed into linker version script.
|
|
static const std::regex VersionScriptSymbolRegex(
|
|
"^(?:struct|class)(?:\\s+Q_\\w*_EXPORT)?\\s+([\\w:]+)[^;]*(;$)?");
|
|
|
|
// This regex looks for the namespaces that needs to be placed into linker version script.
|
|
static const std::regex VersionScriptNamespaceRegex(
|
|
"^namespace\\s+Q_\\w+_EXPORT\\s+([\\w:]+).*");
|
|
|
|
// This regex filters the tailing colon from the symbol name.
|
|
static const std::regex TrailingColonRegex("([\\w]+):$");
|
|
|
|
switch (m_versionScriptGeneratorState) {
|
|
case Ignore:
|
|
scannerDebug() << "line ignored: " << buffer << std::endl;
|
|
m_versionScriptGeneratorState = Active;
|
|
return;
|
|
case Stopped:
|
|
return;
|
|
case IgnoreNext:
|
|
m_versionScriptGeneratorState = Ignore;
|
|
break;
|
|
case Active:
|
|
break;
|
|
}
|
|
|
|
if (buffer.empty())
|
|
return;
|
|
|
|
std::smatch match;
|
|
std::string symbol;
|
|
if (std::regex_match(buffer, match, VersionScriptSymbolRegex) && match[2].str().empty())
|
|
symbol = match[1].str();
|
|
else if (std::regex_match(buffer, match, VersionScriptNamespaceRegex))
|
|
symbol = match[1].str();
|
|
|
|
if (std::regex_match(symbol, match, TrailingColonRegex))
|
|
symbol = match[1].str();
|
|
|
|
// checkLineForSymbols(buffer, symbol);
|
|
if (!symbol.empty() && symbol[symbol.size() - 1] != ';') {
|
|
std::string relPath = m_currentFileInSourceDir
|
|
? std::filesystem::relative(m_currentFile, m_commandLineArgs->sourceDir())
|
|
.string()
|
|
: std::filesystem::relative(m_currentFile, m_commandLineArgs->binaryDir())
|
|
.string();
|
|
|
|
std::string versionStringRecord = " *";
|
|
size_t startPos = 0;
|
|
size_t endPos = 0;
|
|
while (endPos != std::string::npos) {
|
|
endPos = symbol.find("::", startPos);
|
|
size_t length = endPos != std::string::npos ? (endPos - startPos)
|
|
: (symbol.size() - startPos);
|
|
if (length > 0) {
|
|
std::string symbolPart = symbol.substr(startPos, length);
|
|
versionStringRecord += std::to_string(symbolPart.size());
|
|
versionStringRecord += symbolPart;
|
|
}
|
|
startPos = endPos + 2;
|
|
}
|
|
versionStringRecord += "*;";
|
|
if (versionStringRecord.size() < LinkerScriptCommentAlignment)
|
|
versionStringRecord +=
|
|
std::string(LinkerScriptCommentAlignment - versionStringRecord.size(), ' ');
|
|
versionStringRecord += " # ";
|
|
versionStringRecord += relPath;
|
|
versionStringRecord += ":";
|
|
versionStringRecord += std::to_string(m_currentFileLineNumber);
|
|
versionStringRecord += "\n";
|
|
result.versionScriptContent.push_back(versionStringRecord);
|
|
}
|
|
}
|
|
|
|
// The function parses 'headerFile' and collect artifacts that are used at generating step.
|
|
// 'timeStamp' is saved in internal structures to compare it when generating files.
|
|
// '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,
|
|
ParsingResult &result,
|
|
unsigned int skipChecks)
|
|
{
|
|
if (m_commandLineArgs->showOnly())
|
|
std::cout << headerFile << " [" << m_commandLineArgs->moduleName() << "]" << std::endl;
|
|
// This regex checks if line contains a macro.
|
|
static const std::regex MacroRegex("^\\s*#.*");
|
|
|
|
// The regex's bellow check line for known pragmas:
|
|
// - 'qt_sync_skip_header_check' avoid any header checks.
|
|
//
|
|
// - 'qt_sync_stop_processing' stops the header proccesing from a moment when pragma is
|
|
// found. Important note: All the parsing artifacts were found before this point are
|
|
// stored for further processing.
|
|
//
|
|
// - 'qt_sync_suspend_processing' pauses processing and skip lines inside a header until
|
|
// 'qt_sync_resume_processing' is found. 'qt_sync_stop_processing' stops processing if
|
|
// it's found before the 'qt_sync_resume_processing'.
|
|
//
|
|
// - 'qt_sync_resume_processing' resumes processing after 'qt_sync_suspend_processing'.
|
|
//
|
|
// - 'qt_class(<symbol>)' manually declares the 'symbol' that should be used to generate
|
|
// the CaMeL case header alias.
|
|
//
|
|
// - 'qt_deprecates(<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.
|
|
// 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 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$");
|
|
static const std::regex ResumeProcessingRegex("^#\\s*pragma qt_sync_resume_processing$");
|
|
static const std::regex ExplixitClassPragmaRegex("^#\\s*pragma qt_class\\(([^\\)]+)\\)$");
|
|
static const std::regex DeprecatesPragmaRegex("^#\\s*pragma qt_deprecates\\(([^\\)]+)\\)$");
|
|
static const std::regex NoMasterIncludePragmaRegex("^#\\s*pragma qt_no_master_include$");
|
|
|
|
// This regex checks if header contains 'We mean it' disclaimer. All private headers should
|
|
// contain them.
|
|
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_]+)?$");
|
|
static const std::regex EndNamespaceRegex("^QT_END_NAMESPACE(_[A-Z_]+)?$");
|
|
|
|
// This regex checks if line contains the include macro of the following formats:
|
|
// - #include <file>
|
|
// - #include "file"
|
|
// - # include <file>
|
|
static const std::regex IncludeRegex("^#\\s*include\\s*[<\"](.+)[>\"]");
|
|
|
|
// This regex checks if line contains namespace definition.
|
|
static const std::regex NamespaceRegex("\\s*namespace ([^ ]*)\\s+");
|
|
|
|
// This regex checks if line contains the Qt iterator declaration, that need to have
|
|
// CaMel case header alias.
|
|
static const std::regex DeclareIteratorRegex("^ *Q_DECLARE_\\w*ITERATOR\\((\\w+)\\);?$");
|
|
|
|
// This regex checks if header file contains the QT_REQUIRE_CONFIG call.
|
|
// The macro argument is used to wrap an include of the header file inside the module master
|
|
// header file with the '#if QT_CONFIG(<feature>)' guard.
|
|
static const std::regex RequireConfigRegex("^ *QT_REQUIRE_CONFIG\\((\\w+)\\);?$");
|
|
|
|
// 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 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).*");
|
|
|
|
std::ifstream input(headerFile, std::ifstream::in);
|
|
if (!input.is_open()) {
|
|
std::cerr << "Unable to open " << headerFile << std::endl;
|
|
return false;
|
|
}
|
|
|
|
bool hasQtBeginNamespace = false;
|
|
std::string qtBeginNamespace;
|
|
std::string qtEndNamespace;
|
|
bool hasWeMeantIt = false;
|
|
bool isSuspended = false;
|
|
bool isMultiLineComment = false;
|
|
std::size_t bracesDepth = 0;
|
|
std::size_t namespaceCount = 0;
|
|
std::string namespaceString;
|
|
|
|
std::smatch match;
|
|
|
|
std::string buffer;
|
|
std::string line;
|
|
std::string tmpLine;
|
|
std::size_t linesProcessed = 0;
|
|
int faults = NoChecks;
|
|
|
|
// Read file line by line
|
|
while (std::getline(input, tmpLine)) {
|
|
++m_currentFileLineNumber;
|
|
line.append(tmpLine);
|
|
if (line.empty() || line.at(line.size() - 1) == '\\') {
|
|
continue;
|
|
}
|
|
buffer.clear();
|
|
buffer.reserve(line.size());
|
|
// Optimize processing by looking for a special sequences such as:
|
|
// - start-end of comments
|
|
// - 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()) {
|
|
if (line[i + 1] == '*') {
|
|
isMultiLineComment = true;
|
|
continue;
|
|
} else if (line[i + 1] == '/') { // Single line comment
|
|
if (!(skipChecks & WeMeantItChecks)
|
|
&& line.find(WeMeantItString) != std::string::npos) {
|
|
hasWeMeantIt = true;
|
|
continue;
|
|
}
|
|
if (m_versionScriptGeneratorState != Stopped
|
|
&& std::regex_match(line, match, ElfVersionTagRegex)) {
|
|
if (match[1].str() == "ignore")
|
|
m_versionScriptGeneratorState = Ignore;
|
|
else if (match[1].str() == "ignore-next")
|
|
m_versionScriptGeneratorState = IgnoreNext;
|
|
else if (match[1].str() == "stop")
|
|
m_versionScriptGeneratorState = Stopped;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} else if (line[i] == '*' && (i + 1) < line.size() && line[i + 1] == '/') {
|
|
++i;
|
|
isMultiLineComment = false;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (isMultiLineComment) {
|
|
if (!(skipChecks & WeMeantItChecks) &&
|
|
line.find(WeMeantItString) != std::string::npos) {
|
|
hasWeMeantIt = true;
|
|
continue;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (line[i] == '{') {
|
|
if (std::regex_match(buffer, match, NamespaceRegex)) {
|
|
++namespaceCount;
|
|
namespaceString += "::";
|
|
namespaceString += match[1].str();
|
|
}
|
|
++bracesDepth;
|
|
continue;
|
|
} else if (line[i] == '}') {
|
|
if (namespaceCount > 0 && bracesDepth == namespaceCount) {
|
|
namespaceString.resize(namespaceString.rfind("::"));
|
|
--namespaceCount;
|
|
}
|
|
--bracesDepth;
|
|
} else if (bracesDepth == namespaceCount) {
|
|
buffer += line[i];
|
|
}
|
|
}
|
|
line.clear();
|
|
|
|
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 & RhiHeader);
|
|
|
|
// Parse pragmas
|
|
if (std::regex_match(buffer, MacroRegex)) {
|
|
if (std::regex_match(buffer, SkipHeaderCheckRegex)) {
|
|
skipChecks = AllChecks;
|
|
faults = NoChecks;
|
|
} else if (std::regex_match(buffer, StopProcessingRegex)) {
|
|
if (skipChecks == AllChecks)
|
|
m_headerCheckExceptions.push_back(m_currentFileString);
|
|
return true;
|
|
} else if (std::regex_match(buffer, SuspendProcessingRegex)) {
|
|
isSuspended = true;
|
|
} else if (std::regex_match(buffer, ResumeProcessingRegex)) {
|
|
isSuspended = false;
|
|
} else if (std::regex_match(buffer, match, ExplixitClassPragmaRegex)) {
|
|
if (!skipSymbols) {
|
|
updateSymbolDescriptor(match[1].str(), m_currentFilename,
|
|
SymbolDescriptor::Pragma);
|
|
} else {
|
|
// TODO: warn about skipping symbols that are defined explicitly
|
|
}
|
|
} else if (std::regex_match(buffer, NoMasterIncludePragmaRegex)) {
|
|
result.masterInclude = false;
|
|
} else if (std::regex_match(buffer, match, DeprecatesPragmaRegex)) {
|
|
m_deprecatedHeaders[match[1].str()] =
|
|
m_commandLineArgs->moduleName() + '/' + m_currentFilename;
|
|
} else if (std::regex_match(buffer, match, IncludeRegex) && !isSuspended) {
|
|
if (!(skipChecks & IncludeChecks)) {
|
|
std::string includedHeader = match[1].str();
|
|
if (!(skipChecks & PrivateHeaderChecks)
|
|
&& isHeaderPrivate(std::filesystem::path(includedHeader)
|
|
.filename()
|
|
.generic_string())) {
|
|
faults |= PrivateHeaderChecks;
|
|
std::cerr << ErrorMessagePreamble << m_currentFileString
|
|
<< ":" << m_currentFileLineNumber
|
|
<< " 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 << m_warningMessagePreamble << m_currentFileString
|
|
<< ":" << m_currentFileLineNumber
|
|
<< " includes " << includedHeader
|
|
<< " when it should include "
|
|
<< suggestedHeader << std::endl;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Logic below this line is affected by the 'qt_sync_suspend_processing' and
|
|
// 'qt_sync_resume_processing' pragmas.
|
|
if (isSuspended)
|
|
continue;
|
|
|
|
// Look for the symbols in header file.
|
|
if (!skipSymbols) {
|
|
std::string symbol;
|
|
if (checkLineForSymbols(buffer, symbol)) {
|
|
if (namespaceCount == 0
|
|
|| std::regex_match(namespaceString,
|
|
m_commandLineArgs->publicNamespaceRegex())) {
|
|
updateSymbolDescriptor(symbol, m_currentFilename,
|
|
SymbolDescriptor::Declaration);
|
|
}
|
|
continue;
|
|
} else if (std::regex_match(buffer, match, DeclareIteratorRegex)) {
|
|
std::string iteratorSymbol = match[1].str() + "Iterator";
|
|
updateSymbolDescriptor(std::string("Q") + iteratorSymbol, m_currentFilename,
|
|
SymbolDescriptor::Declaration);
|
|
updateSymbolDescriptor(std::string("QMutable") + iteratorSymbol,
|
|
m_currentFilename, SymbolDescriptor::Declaration);
|
|
continue;
|
|
} else if (std::regex_match(buffer, match, RequireConfigRegex)) {
|
|
result.requireConfig = match[1].str();
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Check for both QT_BEGIN_NAMESPACE and QT_END_NAMESPACE macros are present in the
|
|
// header file.
|
|
if (!(skipChecks & NamespaceChecks)) {
|
|
if (std::regex_match(buffer, match, BeginNamespaceRegex)) {
|
|
qtBeginNamespace = match[1].str();
|
|
hasQtBeginNamespace = true;
|
|
} else if (std::regex_match(buffer, match, EndNamespaceRegex)) {
|
|
qtEndNamespace = match[1].str();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Error out if namespace checks are failed.
|
|
if (!(skipChecks & NamespaceChecks)) {
|
|
if (hasQtBeginNamespace) {
|
|
if (qtBeginNamespace != qtEndNamespace) {
|
|
faults |= NamespaceChecks;
|
|
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 << m_warningMessagePreamble << m_currentFileString
|
|
<< " does not include QT_BEGIN_NAMESPACE" << std::endl;
|
|
}
|
|
}
|
|
|
|
if (!(skipChecks & WeMeantItChecks) && !hasWeMeantIt) {
|
|
faults |= WeMeantItChecks;
|
|
std::cerr << m_warningMessagePreamble << m_currentFileString
|
|
<< " does not have the \"We mean it.\" warning"
|
|
<< std::endl;
|
|
}
|
|
|
|
scannerDebug() << "linesTotal: " << m_currentFileLineNumber
|
|
<< " linesProcessed: " << linesProcessed << std::endl;
|
|
|
|
if (skipChecks == AllChecks)
|
|
m_headerCheckExceptions.push_back(m_currentFileString);
|
|
|
|
// Exit with an error if any of critical checks are present.
|
|
return !(faults & m_criticalChecks);
|
|
}
|
|
|
|
// The function checks if line contains the symbol that needs to have a CaMeL-style alias.
|
|
[[nodiscard]] bool checkLineForSymbols(const std::string &line, std::string &symbol)
|
|
{
|
|
scannerDebug() << "checkLineForSymbols: " << line << std::endl;
|
|
|
|
// This regex checks if line contains class or structure declaration like:
|
|
// - <class|stuct> StructName
|
|
// - template <> class ClassName
|
|
// - class ClassName : [public|protected|private] BaseClassName
|
|
// - class ClassName [final|Q_DECL_FINAL|sealed]
|
|
// And possible combinations of the above variants.
|
|
static const std::regex ClassRegex(
|
|
"^ *(template *<.*> *)?(class|struct) +([^ <>]* "
|
|
"+)?((?!Q_DECL_FINAL|final|sealed)[^<\\s\\:]+) ?(<[^>\\:]*> "
|
|
"?)?\\s*(?:Q_DECL_FINAL|final|sealed)?\\s*((,|:)\\s*(public|protected|private)? "
|
|
"*.*)? *$");
|
|
|
|
// This regex checks if line contains function pointer typedef declaration like:
|
|
// - typedef void (* QFunctionPointerType)(int, char);
|
|
static const std::regex FunctionPointerRegex(
|
|
"^ *typedef *.*\\(\\*(Q[^\\)]+)\\)\\(.*\\); *");
|
|
|
|
// This regex checks if line contains class or structure typedef declaration like:
|
|
// - typedef AnySymbol<char> QAnySymbolType;
|
|
static const std::regex TypedefRegex("^ *typedef\\s+(.*)\\s+(Q\\w+); *$");
|
|
|
|
// This regex checks if symbols is the Qt public symbol. Assume that Qt public symbols start
|
|
// with the capital 'Q'.
|
|
static const std::regex QtClassRegex("^Q\\w+$");
|
|
|
|
std::smatch match;
|
|
if (std::regex_match(line, match, FunctionPointerRegex)) {
|
|
symbol = match[1].str();
|
|
} else if (std::regex_match(line, match, TypedefRegex)) {
|
|
symbol = match[2].str();
|
|
} else if (std::regex_match(line, match, ClassRegex)) {
|
|
symbol = match[4].str();
|
|
if (!std::regex_match(symbol, QtClassRegex))
|
|
symbol.clear();
|
|
} else {
|
|
return false;
|
|
}
|
|
return !symbol.empty();
|
|
}
|
|
|
|
[[nodiscard]] bool isHeaderQpa(const std::string &headerFileName)
|
|
{
|
|
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 isHeaderPrivate(const std::string &headerFile)
|
|
{
|
|
return std::regex_match(headerFile, m_commandLineArgs->privateHeadersRegex());
|
|
}
|
|
|
|
[[nodiscard]] bool isHeaderPCH(const std::string &headerFilename)
|
|
{
|
|
static const std::string pchSuffix("_pch.h");
|
|
return headerFilename.find(pchSuffix, headerFilename.size() - pchSuffix.size())
|
|
!= std::string::npos;
|
|
}
|
|
|
|
[[nodiscard]] bool isHeader(const std::filesystem::path &path)
|
|
{
|
|
return path.extension().string() == ".h";
|
|
}
|
|
|
|
[[nodiscard]] bool isDocFileHeuristic(const std::string &headerFilePath)
|
|
{
|
|
return headerFilePath.find("/doc/") != std::string::npos;
|
|
}
|
|
|
|
[[nodiscard]] bool isHeaderGenerated(const std::string &header)
|
|
{
|
|
return m_commandLineArgs->generatedHeaders().find(header)
|
|
!= m_commandLineArgs->generatedHeaders().end();
|
|
}
|
|
|
|
[[nodiscard]] bool generateQtCamelCaseFileIfContentChanged(const std::string &outputFilePath,
|
|
const std::string &aliasedFilePath);
|
|
|
|
[[nodiscard]] bool generateAliasedHeaderFileIfTimestampChanged(
|
|
const std::string &outputFilePath, const std::string &aliasedFilePath,
|
|
const FileStamp &originalStamp = FileStamp::clock::now());
|
|
|
|
bool writeIfDifferent(const std::string &outputFile, const std::string &buffer);
|
|
|
|
[[nodiscard]] bool generateMasterHeader()
|
|
{
|
|
if (m_masterHeaderContents.empty())
|
|
return true;
|
|
|
|
std::string outputFile =
|
|
m_commandLineArgs->includeDir() + '/' + m_commandLineArgs->moduleName();
|
|
|
|
std::string moduleUpper = utils::asciiToUpper(m_commandLineArgs->moduleName());
|
|
std::stringstream buffer;
|
|
buffer << "#ifndef QT_" << moduleUpper << "_MODULE_H\n"
|
|
<< "#define QT_" << moduleUpper << "_MODULE_H\n"
|
|
<< "#include <" << m_commandLineArgs->moduleName() << "/"
|
|
<< m_commandLineArgs->moduleName() << "Depends>\n";
|
|
for (const auto &headerContents : m_masterHeaderContents) {
|
|
if (!headerContents.second.empty()) {
|
|
buffer << "#if QT_CONFIG(" << headerContents.second << ")\n"
|
|
<< "#include \"" << headerContents.first << "\"\n"
|
|
<< "#endif\n";
|
|
} else {
|
|
buffer << "#include \"" << headerContents.first << "\"\n";
|
|
}
|
|
}
|
|
buffer << "#endif\n";
|
|
|
|
m_producedHeaders.insert(m_commandLineArgs->moduleName());
|
|
return writeIfDifferent(outputFile, buffer.str());
|
|
}
|
|
|
|
[[nodiscard]] bool generateVersionHeader(const std::string &outputFile)
|
|
{
|
|
std::string moduleNameUpper = utils::asciiToUpper( m_commandLineArgs->moduleName());
|
|
|
|
std::stringstream buffer;
|
|
buffer << "/* This file was generated by syncqt. */\n"
|
|
<< "#ifndef QT_" << moduleNameUpper << "_VERSION_H\n"
|
|
<< "#define QT_" << moduleNameUpper << "_VERSION_H\n\n"
|
|
<< "#define " << moduleNameUpper << "_VERSION_STR \"" << QT_VERSION_STR << "\"\n\n"
|
|
<< "#define " << moduleNameUpper << "_VERSION "
|
|
<< "0x0" << QT_VERSION_MAJOR << "0" << QT_VERSION_MINOR << "0" << QT_VERSION_PATCH
|
|
<< "\n\n"
|
|
<< "#endif // QT_" << moduleNameUpper << "_VERSION_H\n";
|
|
|
|
return writeIfDifferent(outputFile, buffer.str());
|
|
}
|
|
|
|
[[nodiscard]] bool generateDeprecatedHeaders()
|
|
{
|
|
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) {
|
|
const std::string &descriptor = it->first;
|
|
const std::string &replacement = it->second;
|
|
|
|
const auto separatorPos = descriptor.find(',');
|
|
std::string headerName = 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, minor, major)) {
|
|
std::cerr << ErrorMessagePreamble
|
|
<< "Invalid version format specified for the deprecated header file "
|
|
<< headerName << ": '" << 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 << headerName
|
|
<< " 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;
|
|
}
|
|
}
|
|
|
|
std::string qualifiedHeaderName =
|
|
std::regex_replace(headerName, cIdentifierSymbolsRegex, "_");
|
|
std::string guard = guard_base + "_" + qualifiedHeaderName;
|
|
std::string warningText = "Header <" + m_commandLineArgs->moduleName() + "/"
|
|
+ headerName + "> is deprecated" + versionDisclaimer + ". Please include <"
|
|
+ replacement + "> instead.";
|
|
std::stringstream buffer;
|
|
buffer << "#ifndef " << guard << "\n"
|
|
<< "#define " << guard << "\n"
|
|
<< "#if defined(__GNUC__)\n"
|
|
<< "# warning " << warningText << "\n"
|
|
<< "#elif defined(_MSC_VER)\n"
|
|
<< "# pragma message (\"" << warningText << "\")\n"
|
|
<< "#endif\n"
|
|
<< "#include <" << replacement << ">\n"
|
|
<< "#endif\n";
|
|
writeIfDifferent(m_commandLineArgs->includeDir() + '/' + headerName, buffer.str());
|
|
m_producedHeaders.insert(headerName);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] bool generateHeaderCheckExceptions()
|
|
{
|
|
std::stringstream buffer;
|
|
for (const auto &header : m_headerCheckExceptions)
|
|
buffer << header << ";";
|
|
return writeIfDifferent(m_commandLineArgs->binaryDir() + '/'
|
|
+ m_commandLineArgs->moduleName()
|
|
+ "_header_check_exceptions",
|
|
buffer.str());
|
|
}
|
|
|
|
[[nodiscard]] bool generateLinkerVersionScript()
|
|
{
|
|
std::stringstream buffer;
|
|
for (const auto &content : m_versionScriptContents)
|
|
buffer << content;
|
|
return writeIfDifferent(m_commandLineArgs->versionScriptFile(), buffer.str());
|
|
}
|
|
|
|
bool updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst);
|
|
void updateSymbolDescriptor(const std::string &symbol, const std::string &file,
|
|
SymbolDescriptor::SourceType type);
|
|
};
|
|
|
|
// The function updates information about the symbol:
|
|
// - The path and modification time of the file where the symbol was found.
|
|
// - The source of finding
|
|
// Also displays a short info about a symbol in show only mode.
|
|
void SyncScanner::updateSymbolDescriptor(const std::string &symbol, const std::string &file,
|
|
SymbolDescriptor::SourceType type)
|
|
{
|
|
if (m_commandLineArgs->showOnly())
|
|
std::cout << " SYMBOL: " << symbol << std::endl;
|
|
m_symbols[symbol].update(file, type);
|
|
}
|
|
|
|
[[nodiscard]] std::filesystem::path
|
|
SyncScanner::makeHeaderAbsolute(const std::string &filename) const
|
|
{
|
|
if (std::filesystem::path(filename).is_relative())
|
|
return utils::normilizedPath(m_commandLineArgs->sourceDir() + '/' + filename);
|
|
|
|
return utils::normilizedPath(filename);
|
|
}
|
|
|
|
bool SyncScanner::updateOrCopy(const std::filesystem::path &src, const std::filesystem::path &dst)
|
|
{
|
|
if (m_commandLineArgs->showOnly())
|
|
return true;
|
|
|
|
if (src == dst) {
|
|
std::cout << "Source and destination paths are same when copying " << src.string()
|
|
<< ". Skipping." << std::endl;
|
|
return true;
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::copy(src, dst, std::filesystem::copy_options::update_existing, ec);
|
|
if (ec) {
|
|
ec.clear();
|
|
std::filesystem::remove(dst, ec);
|
|
if (ec) {
|
|
// On some file systems(e.g. vboxfs) the std::filesystem::copy doesn't support
|
|
// std::filesystem::copy_options::overwrite_existing remove file first and then copy.
|
|
std::cerr << "Unable to remove file: " << src << " to " << dst << " error: ("
|
|
<< ec.value() << ")" << ec.message() << std::endl;
|
|
return false;
|
|
}
|
|
|
|
std::filesystem::copy(src, dst, std::filesystem::copy_options::overwrite_existing, ec);
|
|
if (ec) {
|
|
std::cerr << "Unable to copy file: " << src << " to " << dst << " error: ("
|
|
<< ec.value() << ")" << ec.message() << std::endl;
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// The function generates Qt CaMeL-case files.
|
|
// CaMeL-case files can have timestamp that is the same as or newer than timestamp of file that
|
|
// supposed to included there. It's not an issue when we generate regular aliases because the
|
|
// content of aliases is always the same, but we only want to "touch" them when content of original
|
|
// is changed.
|
|
bool SyncScanner::generateQtCamelCaseFileIfContentChanged(const std::string &outputFilePath,
|
|
const std::string &aliasedFilePath)
|
|
{
|
|
if (m_commandLineArgs->showOnly())
|
|
return true;
|
|
|
|
std::string buffer = "#include \"";
|
|
buffer += aliasedFilePath;
|
|
buffer += "\"\n";
|
|
|
|
return writeIfDifferent(outputFilePath, buffer);
|
|
}
|
|
|
|
// The function generates aliases for files in source tree. Since the content of these aliases is
|
|
// always same, it's ok to check only timestamp and touch files in case if stamp of original is
|
|
// newer than the timestamp of an alias.
|
|
bool SyncScanner::generateAliasedHeaderFileIfTimestampChanged(const std::string &outputFilePath,
|
|
const std::string &aliasedFilePath,
|
|
const FileStamp &originalStamp)
|
|
{
|
|
if (m_commandLineArgs->showOnly())
|
|
return true;
|
|
|
|
if (std::filesystem::exists({ outputFilePath })
|
|
&& std::filesystem::last_write_time({ outputFilePath }) >= originalStamp) {
|
|
return true;
|
|
}
|
|
scannerDebug() << "Rewrite " << outputFilePath << std::endl;
|
|
|
|
std::ofstream ofs;
|
|
ofs.open(outputFilePath, std::ofstream::out | std::ofstream::trunc);
|
|
if (!ofs.is_open()) {
|
|
std::cerr << "Unable to write header file alias: " << outputFilePath << std::endl;
|
|
return false;
|
|
}
|
|
ofs << "#include \"" << aliasedFilePath << "\"\n";
|
|
ofs.close();
|
|
return true;
|
|
}
|
|
|
|
bool SyncScanner::writeIfDifferent(const std::string &outputFile, const std::string &buffer)
|
|
{
|
|
if (m_commandLineArgs->showOnly())
|
|
return true;
|
|
|
|
static const std::streamsize bufferSize = 1025;
|
|
bool differs = false;
|
|
std::filesystem::path outputFilePath(outputFile);
|
|
|
|
std::string outputDirectory = outputFilePath.parent_path().string();
|
|
if (!std::filesystem::exists(outputDirectory))
|
|
std::filesystem::create_directories(outputDirectory);
|
|
|
|
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)
|
|
&& expectedSize == std::filesystem::file_size(outputFilePath)) {
|
|
char rdBuffer[bufferSize];
|
|
memset(rdBuffer, 0, bufferSize);
|
|
|
|
std::ifstream ifs(outputFile, std::fstream::in);
|
|
std::streamsize currentPos = 0;
|
|
|
|
std::size_t bytesRead = 0;
|
|
do {
|
|
ifs.read(rdBuffer, bufferSize - 1); // Read by 1K
|
|
bytesRead = ifs.gcount();
|
|
if (buffer.compare(currentPos, bytesRead, rdBuffer) != 0) {
|
|
differs = true;
|
|
break;
|
|
}
|
|
currentPos += bytesRead;
|
|
memset(rdBuffer, 0, bufferSize);
|
|
} while (bytesRead > 0);
|
|
|
|
ifs.close();
|
|
} else {
|
|
differs = true;
|
|
}
|
|
|
|
scannerDebug() << "Update: " << outputFile << " " << differs << std::endl;
|
|
if (differs) {
|
|
std::ofstream ofs;
|
|
ofs.open(outputFilePath, std::fstream::out | std::ofstream::trunc);
|
|
if (!ofs.is_open()) {
|
|
std::cerr << "Unable to write header content to " << outputFilePath << std::endl;
|
|
return false;
|
|
}
|
|
ofs << buffer;
|
|
|
|
ofs.close();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
CommandLineOptions options(argc, argv);
|
|
if (!options.isValid())
|
|
return InvalidArguments;
|
|
|
|
if (options.printHelpOnly()) {
|
|
options.printHelp();
|
|
return NoError;
|
|
}
|
|
|
|
SyncScanner scanner = SyncScanner(&options);
|
|
return scanner.sync();
|
|
}
|