Teach moc to output a Make-style depfile
If moc is invoked with the --output-dep-file option, it will generate a "moc_<source_file_name>.d" dep file which contains dependency entries that can be consumed by a Makefile / Ninja build system. This is useful for build tools (like CMake) to know when moc should be re-ran. In the future, it might also be useful for ccache (teach ccache not to re-run moc when not necessary). The dependency list contains: the original source file, the passed --include files (like moc_predefs.h), the include files that were discovered while preprocessing the source file, and the plugin metadata json files listed in Q_PLUGIN_METADATA macros. The file paths are encoded using QFile::encodeName, so using the local 8-bit encoding. The paths are also escaped (so ' ' replaced by '\ ', '$' by '$$', etc) according to the Make-style rules as described in clang's dep file generator https://github.com/llvm/llvm-project/blob/release/9.x/clang/lib/Frontend/DependencyFile.cpp#L233 For reference, the equivalent Ninja depfile parser source code can be found at https://github.com/ninja-build/ninja/blob/v1.9.0/src/depfile_parser.in.cc#L37 Additional options that can be passed: --dep-file-path - to change the location where the dep file should be generated. --dep-file-rule-name - to change the rule name (first line) of the dep file (useful when no -o option is specified, so output goes to stdout). Encoding story. Note that moc doesn't handle non-local-8-bit characters properly when processing include directives at the preprocessor step. Specifically the content of the main input file is read as a raw byte array (which can be UTF-8 encoded) and then each include directive is resolved via Preprocessor::resolveInclude(), which calls QString::fromLocal8Bit(). Because moc uses the QtBootstrap library, only a limited set of codecs are available: various UTF 8 / 16 / 32 codecs and QLatin1Codec (ISO-8859-15). This means that on Windows, if the source input file is UTF-8 encoded, and contains include names with UTF-8 characters (like an emoji or any character >= 127 that is not in the QLatin1 codec), moc will fail to resolve and process that include, and thus no dep file entry will be created either. On macOS / QNX / WASM the main locale is UTF-8, so file content and paths will be processed correctly (hardcoded via QT_LOCALE_IS_UTF8 in src/corelib/codecs/qtextcodec_p.h). On Linux it will depend on the current locale / encoding set, and if that encoding is one of the ones supported above. UTF-8 should work fine. [ChangeLog][QtCore][moc] moc can now output a ".d" dep file that can be consumed by other build systems. Task-number: QTBUG-74521 Task-number: QTBUG-76598 Change-Id: I5585631ff1bbbae4e2875cade9cb6c20ed018c0a Reviewed-by: Leander Beernaert <leander.beernaert@qt.io> Reviewed-by: Joerg Bornemann <joerg.bornemann@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
parent
18f22fea7c
commit
cf3d4cf3c3
@ -175,6 +175,49 @@ static QStringList argumentsFromCommandLineAndFile(const QStringList &arguments)
|
||||
return allArguments;
|
||||
}
|
||||
|
||||
// Escape characters in given path. Dependency paths are Make-style, not NMake/Jom style.
|
||||
// The paths can also be consumed by Ninja.
|
||||
// "$" replaced by "$$"
|
||||
// "#" replaced by "\#"
|
||||
// " " replaced by "\ "
|
||||
// "\#" replaced by "\\#"
|
||||
// "\ " replaced by "\\\ "
|
||||
//
|
||||
// The escape rules are according to what clang / llvm escapes when generating a Make-style
|
||||
// dependency file.
|
||||
// Is a template function, because input param can be either a QString or a QByteArray.
|
||||
template <typename T> struct CharType;
|
||||
template <> struct CharType<QString> { using type = QLatin1Char; };
|
||||
template <> struct CharType<QByteArray> { using type = char; };
|
||||
template <typename StringType>
|
||||
StringType escapeDependencyPath(const StringType &path)
|
||||
{
|
||||
using CT = typename CharType<StringType>::type;
|
||||
StringType escapedPath;
|
||||
int size = path.size();
|
||||
escapedPath.reserve(size);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
if (path[i] == CT('$')) {
|
||||
escapedPath.append(CT('$'));
|
||||
} else if (path[i] == CT('#')) {
|
||||
escapedPath.append(CT('\\'));
|
||||
} else if (path[i] == CT(' ')) {
|
||||
escapedPath.append(CT('\\'));
|
||||
int backwards_it = i - 1;
|
||||
while (backwards_it > 0 && path[backwards_it] == CT('\\')) {
|
||||
escapedPath.append(CT('\\'));
|
||||
--backwards_it;
|
||||
}
|
||||
}
|
||||
escapedPath.append(path[i]);
|
||||
}
|
||||
return escapedPath;
|
||||
}
|
||||
|
||||
QByteArray escapeAndEncodeDependencyPath(const QString &path)
|
||||
{
|
||||
return QFile::encodeName(escapeDependencyPath(path));
|
||||
}
|
||||
|
||||
int runMoc(int argc, char **argv)
|
||||
{
|
||||
@ -308,6 +351,22 @@ int runMoc(int argc, char **argv)
|
||||
collectOption.setDescription(QStringLiteral("Instead of processing C++ code, collect previously generated JSON output into a single file."));
|
||||
parser.addOption(collectOption);
|
||||
|
||||
QCommandLineOption depFileOption(QStringLiteral("output-dep-file"));
|
||||
depFileOption.setDescription(
|
||||
QStringLiteral("Output a Make-style dep file for build system consumption."));
|
||||
parser.addOption(depFileOption);
|
||||
|
||||
QCommandLineOption depFilePathOption(QStringLiteral("dep-file-path"));
|
||||
depFilePathOption.setDescription(QStringLiteral("Path where to write the dep file."));
|
||||
depFilePathOption.setValueName(QStringLiteral("file"));
|
||||
parser.addOption(depFilePathOption);
|
||||
|
||||
QCommandLineOption depFileRuleNameOption(QStringLiteral("dep-file-rule-name"));
|
||||
depFileRuleNameOption.setDescription(
|
||||
QStringLiteral("The rule name (first line) of the dep file."));
|
||||
depFileRuleNameOption.setValueName(QStringLiteral("rule name"));
|
||||
parser.addOption(depFileRuleNameOption);
|
||||
|
||||
parser.addPositionalArgument(QStringLiteral("[header-file]"),
|
||||
QStringLiteral("Header file to read from, otherwise stdin."));
|
||||
parser.addPositionalArgument(QStringLiteral("[@option-file]"),
|
||||
@ -476,6 +535,7 @@ int runMoc(int argc, char **argv)
|
||||
|
||||
// 1. preprocess
|
||||
const auto includeFiles = parser.values(includeOption);
|
||||
QStringList validIncludesFiles;
|
||||
for (const QString &includeName : includeFiles) {
|
||||
QByteArray rawName = pp.resolveInclude(QFile::encodeName(includeName), moc.filename);
|
||||
if (rawName.isEmpty()) {
|
||||
@ -488,6 +548,7 @@ int runMoc(int argc, char **argv)
|
||||
moc.symbols += Symbol(0, MOC_INCLUDE_BEGIN, rawName);
|
||||
moc.symbols += pp.preprocessed(rawName, &f);
|
||||
moc.symbols += Symbol(0, MOC_INCLUDE_END, rawName);
|
||||
validIncludesFiles.append(includeName);
|
||||
} else {
|
||||
fprintf(stderr, "Warning: Cannot open %s included by moc file %s: %s\n",
|
||||
rawName.constData(),
|
||||
@ -507,6 +568,7 @@ int runMoc(int argc, char **argv)
|
||||
|
||||
QScopedPointer<FILE, ScopedPointerFileCloser> jsonOutput;
|
||||
|
||||
bool outputToFile = true;
|
||||
if (output.size()) { // output file specified
|
||||
#if defined(_MSC_VER)
|
||||
if (_wfopen_s(&out, reinterpret_cast<const wchar_t *>(output.utf16()), L"w") != 0)
|
||||
@ -535,6 +597,7 @@ int runMoc(int argc, char **argv)
|
||||
}
|
||||
} else { // use stdout
|
||||
out = stdout;
|
||||
outputToFile = false;
|
||||
}
|
||||
|
||||
if (pp.preprocessOnly) {
|
||||
@ -549,6 +612,74 @@ int runMoc(int argc, char **argv)
|
||||
if (output.size())
|
||||
fclose(out);
|
||||
|
||||
if (parser.isSet(depFileOption)) {
|
||||
// 4. write a Make-style dependency file (can also be consumed by Ninja).
|
||||
QString depOutputFileName;
|
||||
QString depRuleName = output;
|
||||
|
||||
if (parser.isSet(depFileRuleNameOption))
|
||||
depRuleName = parser.value(depFileRuleNameOption);
|
||||
|
||||
if (parser.isSet(depFilePathOption)) {
|
||||
depOutputFileName = parser.value(depFilePathOption);
|
||||
} else if (outputToFile) {
|
||||
depOutputFileName = output + QLatin1String(".d");
|
||||
} else {
|
||||
fprintf(stderr, "moc: Writing to stdout, but no depfile path specified.\n");
|
||||
}
|
||||
|
||||
QScopedPointer<FILE, ScopedPointerFileCloser> depFileHandle;
|
||||
FILE *depFileHandleRaw;
|
||||
#if defined(_MSC_VER)
|
||||
if (_wfopen_s(&depFileHandleRaw,
|
||||
reinterpret_cast<const wchar_t *>(depOutputFileName.utf16()), L"w") != 0)
|
||||
#else
|
||||
depFileHandleRaw = fopen(QFile::encodeName(depOutputFileName).constData(), "w");
|
||||
if (!depFileHandleRaw)
|
||||
#endif
|
||||
fprintf(stderr, "moc: Cannot create dep output file '%s'. %s\n",
|
||||
QFile::encodeName(depOutputFileName).constData(),
|
||||
strerror(errno));
|
||||
depFileHandle.reset(depFileHandleRaw);
|
||||
|
||||
if (!depFileHandle.isNull()) {
|
||||
// First line is the path to the generated file.
|
||||
fprintf(depFileHandle.data(), "%s: ",
|
||||
escapeAndEncodeDependencyPath(depRuleName).constData());
|
||||
|
||||
QByteArrayList dependencies;
|
||||
|
||||
// If there's an input file, it's the first dependency.
|
||||
if (!filename.isEmpty()) {
|
||||
dependencies.append(escapeAndEncodeDependencyPath(filename).constData());
|
||||
}
|
||||
|
||||
// Additional passed-in includes are dependencies (like moc_predefs.h).
|
||||
for (const QString &includeName : validIncludesFiles) {
|
||||
dependencies.append(escapeAndEncodeDependencyPath(includeName).constData());
|
||||
}
|
||||
|
||||
// Plugin metadata json files discovered via Q_PLUGIN_METADATA macros are also
|
||||
// dependencies.
|
||||
for (const QString &pluginMetadataFile : moc.parsedPluginMetadataFiles) {
|
||||
dependencies.append(escapeAndEncodeDependencyPath(pluginMetadataFile).constData());
|
||||
}
|
||||
|
||||
// All pre-processed includes are dependnecies.
|
||||
// Sort the entries for easier human consumption.
|
||||
auto includeList = pp.preprocessedIncludes.values();
|
||||
std::sort(includeList.begin(), includeList.end());
|
||||
|
||||
for (QByteArray &includeName : includeList) {
|
||||
dependencies.append(escapeDependencyPath(includeName));
|
||||
}
|
||||
|
||||
// Join dependencies, output them, and output a final new line.
|
||||
const auto dependenciesJoined = dependencies.join(QByteArrayLiteral(" \\\n "));
|
||||
fprintf(depFileHandle.data(), "%s\n", dependenciesJoined.constData());
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -1382,6 +1382,7 @@ void Moc::parsePluginData(ClassDef *def)
|
||||
error(msg.constData());
|
||||
return;
|
||||
}
|
||||
parsedPluginMetadataFiles.append(fi.canonicalFilePath());
|
||||
metaData = file.readAll();
|
||||
}
|
||||
}
|
||||
|
@ -223,6 +223,7 @@ public:
|
||||
QHash<QByteArray, QByteArray> knownQObjectClasses;
|
||||
QHash<QByteArray, QByteArray> knownGadgets;
|
||||
QMap<QString, QJsonArray> metaArgs;
|
||||
QVector<QString> parsedPluginMetadataFiles;
|
||||
|
||||
void parse();
|
||||
void generate(FILE *out, FILE *jsonOutput);
|
||||
|
Loading…
x
Reference in New Issue
Block a user