// Copyright (C) 2016 The Qt Company Ltd. // Copyright (C) 2016 Intel Corporation. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include "preprocessor.h" #include "moc.h" #include "outputrevision.h" #include "collectjson.h" #include #include #include #include #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; /* This function looks at two file names and returns the name of the infile with a path relative to outfile. Examples: /tmp/abc, /tmp/bcd -> abc xyz/a/bc, xyz/b/ac -> ../a/bc /tmp/abc, xyz/klm -> /tmp/abc */ static QByteArray combinePath(const QString &infile, const QString &outfile) { QFileInfo inFileInfo(QDir::current(), infile); QFileInfo outFileInfo(QDir::current(), outfile); const QByteArray relativePath = QFile::encodeName(outFileInfo.dir().relativeFilePath(inFileInfo.filePath())); #ifdef Q_OS_WIN // It's a system limitation. // It depends on the Win API function which is used by the program to open files. // cl apparently uses the functions that have the MAX_PATH limitation. if (outFileInfo.dir().absolutePath().length() + relativePath.length() + 1 >= 260) return QFile::encodeName(inFileInfo.absoluteFilePath()); #endif return relativePath; } void error(const char *msg = "Invalid argument") { if (msg) fprintf(stderr, "moc: %s\n", msg); } struct ScopedPointerFileCloser { static inline void cleanup(FILE *handle) { if (handle) fclose(handle); } }; static inline bool hasNext(const Symbols &symbols, int i) { return (i < symbols.size()); } static inline const Symbol &next(const Symbols &symbols, int &i) { return symbols.at(i++); } QByteArray composePreprocessorOutput(const Symbols &symbols) { QByteArray output; int lineNum = 1; Token last = PP_NOTOKEN; Token secondlast = last; int i = 0; while (hasNext(symbols, i)) { Symbol sym = next(symbols, i); switch (sym.token) { case PP_NEWLINE: case PP_WHITESPACE: if (last != PP_WHITESPACE) { secondlast = last; last = PP_WHITESPACE; output += ' '; } continue; case PP_STRING_LITERAL: if (last == PP_STRING_LITERAL) output.chop(1); else if (secondlast == PP_STRING_LITERAL && last == PP_WHITESPACE) output.chop(2); else break; output += sym.lexem().mid(1); secondlast = last; last = PP_STRING_LITERAL; continue; case MOC_INCLUDE_BEGIN: lineNum = 0; continue; case MOC_INCLUDE_END: lineNum = sym.lineNum; continue; default: break; } secondlast = last; last = sym.token; const int padding = sym.lineNum - lineNum; if (padding > 0) { output.resize(output.size() + padding); memset(output.data() + output.size() - padding, '\n', padding); lineNum = sym.lineNum; } output += sym.lexem(); } return output; } static QStringList argumentsFromCommandLineAndFile(const QStringList &arguments, bool &hasOptionFiles) { QStringList allArguments; hasOptionFiles = false; allArguments.reserve(arguments.size()); for (const QString &argument : arguments) { // "@file" doesn't start with a '-' so we can't use QCommandLineParser for it if (argument.startsWith(u'@')) { QString optionsFile = argument; optionsFile.remove(0, 1); if (optionsFile.isEmpty()) { error("The @ option requires an input file"); return QStringList(); } QFile f(optionsFile); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { error("Cannot open options file specified with @"); return QStringList(); } hasOptionFiles = true; while (!f.atEnd()) { QString line = QString::fromLocal8Bit(f.readLine().trimmed()); if (!line.isEmpty()) allArguments << line; } } else { allArguments << argument; } } return allArguments; } int runMoc(int argc, char **argv) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationVersion(QString::fromLatin1(QT_VERSION_STR)); bool autoInclude = true; bool defaultInclude = true; Preprocessor pp; Moc moc; pp.macros["Q_MOC_RUN"]; pp.macros["__cplusplus"]; // Don't stumble over GCC extensions Macro dummyVariadicFunctionMacro; dummyVariadicFunctionMacro.isFunction = true; dummyVariadicFunctionMacro.isVariadic = true; dummyVariadicFunctionMacro.arguments += Symbol(0, PP_IDENTIFIER, "__VA_ARGS__"); pp.macros["__attribute__"] = dummyVariadicFunctionMacro; pp.macros["__declspec"] = dummyVariadicFunctionMacro; QString filename; QString output; QFile in; FILE *out = nullptr; // Note that moc isn't translated. // If you use this code as an example for a translated app, make sure to translate the strings. QCommandLineParser parser; parser.setApplicationDescription(QStringLiteral("Qt Meta Object Compiler version %1 (Qt %2)") .arg(mocOutputRevision).arg(QString::fromLatin1(QT_VERSION_STR))); parser.addHelpOption(); parser.addVersionOption(); parser.setSingleDashWordOptionMode(QCommandLineParser::ParseAsLongOptions); QCommandLineOption outputOption(QStringLiteral("o")); outputOption.setDescription(QStringLiteral("Write output to file rather than stdout.")); outputOption.setValueName(QStringLiteral("file")); outputOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(outputOption); QCommandLineOption includePathOption(QStringLiteral("I")); includePathOption.setDescription(QStringLiteral("Add dir to the include path for header files.")); includePathOption.setValueName(QStringLiteral("dir")); includePathOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(includePathOption); QCommandLineOption macFrameworkOption(QStringLiteral("F")); macFrameworkOption.setDescription(QStringLiteral("Add Mac framework to the include path for header files.")); macFrameworkOption.setValueName(QStringLiteral("framework")); macFrameworkOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(macFrameworkOption); QCommandLineOption preprocessOption(QStringLiteral("E")); preprocessOption.setDescription(QStringLiteral("Preprocess only; do not generate meta object code.")); parser.addOption(preprocessOption); QCommandLineOption defineOption(QStringLiteral("D")); defineOption.setDescription(QStringLiteral("Define macro, with optional definition.")); defineOption.setValueName(QStringLiteral("macro[=def]")); defineOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(defineOption); QCommandLineOption undefineOption(QStringLiteral("U")); undefineOption.setDescription(QStringLiteral("Undefine macro.")); undefineOption.setValueName(QStringLiteral("macro")); undefineOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(undefineOption); QCommandLineOption metadataOption(QStringLiteral("M")); metadataOption.setDescription(QStringLiteral("Add key/value pair to plugin meta data")); metadataOption.setValueName(QStringLiteral("key=value")); metadataOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(metadataOption); QCommandLineOption compilerFlavorOption(QStringLiteral("compiler-flavor")); compilerFlavorOption.setDescription(QStringLiteral("Set the compiler flavor: either \"msvc\" or \"unix\".")); compilerFlavorOption.setValueName(QStringLiteral("flavor")); parser.addOption(compilerFlavorOption); QCommandLineOption noIncludeOption(QStringLiteral("i")); noIncludeOption.setDescription(QStringLiteral("Do not generate an #include statement.")); parser.addOption(noIncludeOption); QCommandLineOption pathPrefixOption(QStringLiteral("p")); pathPrefixOption.setDescription(QStringLiteral("Path prefix for included file.")); pathPrefixOption.setValueName(QStringLiteral("path")); pathPrefixOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(pathPrefixOption); QCommandLineOption forceIncludeOption(QStringLiteral("f")); forceIncludeOption.setDescription(QStringLiteral("Force #include (overwrite default).")); forceIncludeOption.setValueName(QStringLiteral("file")); forceIncludeOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(forceIncludeOption); QCommandLineOption prependIncludeOption(QStringLiteral("b")); prependIncludeOption.setDescription(QStringLiteral("Prepend #include (preserve default include).")); prependIncludeOption.setValueName(QStringLiteral("file")); prependIncludeOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(prependIncludeOption); QCommandLineOption includeOption(QStringLiteral("include")); includeOption.setDescription(QStringLiteral("Parse as an #include before the main source(s).")); includeOption.setValueName(QStringLiteral("file")); parser.addOption(includeOption); QCommandLineOption noNotesWarningsCompatOption(QStringLiteral("n")); noNotesWarningsCompatOption.setDescription(QStringLiteral("Do not display notes (-nn) or warnings (-nw). Compatibility option.")); noNotesWarningsCompatOption.setValueName(QStringLiteral("which")); noNotesWarningsCompatOption.setFlags(QCommandLineOption::ShortOptionStyle); parser.addOption(noNotesWarningsCompatOption); QCommandLineOption noNotesOption(QStringLiteral("no-notes")); noNotesOption.setDescription(QStringLiteral("Do not display notes.")); parser.addOption(noNotesOption); QCommandLineOption noWarningsOption(QStringLiteral("no-warnings")); noWarningsOption.setDescription(QStringLiteral("Do not display warnings (implies --no-notes).")); parser.addOption(noWarningsOption); QCommandLineOption ignoreConflictsOption(QStringLiteral("ignore-option-clashes")); ignoreConflictsOption.setDescription(QStringLiteral("Ignore all options that conflict with compilers, like -pthread conflicting with moc's -p option.")); parser.addOption(ignoreConflictsOption); QCommandLineOption jsonOption(QStringLiteral("output-json")); jsonOption.setDescription(QStringLiteral("In addition to generating C++ code, create a machine-readable JSON file in a file that matches the output file and an extra .json extension.")); parser.addOption(jsonOption); QCommandLineOption debugIncludesOption(QStringLiteral("debug-includes")); debugIncludesOption.setDescription(QStringLiteral("Display debug messages of each considered include path.")); parser.addOption(debugIncludesOption); QCommandLineOption collectOption(QStringLiteral("collect-json")); 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); QCommandLineOption requireCompleTypesOption(QStringLiteral("require-complete-types")); requireCompleTypesOption.setDescription(QStringLiteral("Require complete types for better performance")); parser.addOption(requireCompleTypesOption); parser.addPositionalArgument(QStringLiteral("[header-file]"), QStringLiteral("Header file to read from, otherwise stdin.")); parser.addPositionalArgument(QStringLiteral("[@option-file]"), QStringLiteral("Read additional options from option-file.")); parser.addPositionalArgument(QStringLiteral("[MOC generated json file]"), QStringLiteral("MOC generated json output")); bool hasOptionFiles = false; const QStringList arguments = argumentsFromCommandLineAndFile(app.arguments(), hasOptionFiles); if (arguments.isEmpty()) return 1; parser.process(arguments); const QStringList files = parser.positionalArguments(); output = parser.value(outputOption); if (parser.isSet(collectOption)) return collectJson(files, output, hasOptionFiles); if (files.size() > 1) { error(qPrintable("Too many input files specified: '"_L1 + files.join("' '"_L1) + u'\'')); parser.showHelp(1); } else if (!files.isEmpty()) { filename = files.first(); } const bool ignoreConflictingOptions = parser.isSet(ignoreConflictsOption); pp.preprocessOnly = parser.isSet(preprocessOption); pp.setDebugIncludes(parser.isSet(debugIncludesOption)); if (parser.isSet(noIncludeOption)) { moc.noInclude = true; autoInclude = false; } if (parser.isSet(requireCompleTypesOption)) moc.requireCompleteTypes = true; if (!ignoreConflictingOptions) { if (parser.isSet(forceIncludeOption)) { moc.noInclude = false; autoInclude = false; const auto forceIncludes = parser.values(forceIncludeOption); for (const QString &include : forceIncludes) { moc.includeFiles.append(QFile::encodeName(include)); defaultInclude = false; } } const auto prependIncludes = parser.values(prependIncludeOption); for (const QString &include : prependIncludes) moc.includeFiles.prepend(QFile::encodeName(include)); if (parser.isSet(pathPrefixOption)) moc.includePath = QFile::encodeName(parser.value(pathPrefixOption)); } const auto includePaths = parser.values(includePathOption); for (const QString &path : includePaths) pp.includes += Preprocessor::IncludePath(QFile::encodeName(path)); QString compilerFlavor = parser.value(compilerFlavorOption); if (compilerFlavor.isEmpty() || compilerFlavor == "unix"_L1) { // traditional Unix compilers use both CPATH and CPLUS_INCLUDE_PATH // $CPATH feeds to #include <...> and #include "...", whereas // CPLUS_INCLUDE_PATH is equivalent to GCC's -isystem, so we parse later const auto cpath = qgetenv("CPATH").split(QDir::listSeparator().toLatin1()); for (const QByteArray &p : cpath) pp.includes += Preprocessor::IncludePath(p); const auto cplus_include_path = qgetenv("CPLUS_INCLUDE_PATH").split(QDir::listSeparator().toLatin1()); for (const QByteArray &p : cplus_include_path) pp.includes += Preprocessor::IncludePath(p); } else if (compilerFlavor == "msvc"_L1) { // MSVC uses one environment variable: INCLUDE const auto include = qgetenv("INCLUDE").split(QDir::listSeparator().toLatin1()); for (const QByteArray &p : include) pp.includes += Preprocessor::IncludePath(p); } else { error(qPrintable("Unknown compiler flavor '"_L1 + compilerFlavor + "'; valid values are: msvc, unix."_L1)); parser.showHelp(1); } const auto macFrameworks = parser.values(macFrameworkOption); for (const QString &path : macFrameworks) { // minimalistic framework support for the mac Preprocessor::IncludePath p(QFile::encodeName(path)); p.isFrameworkPath = true; pp.includes += p; } const auto defines = parser.values(defineOption); for (const QString &arg : defines) { QByteArray name = arg.toLocal8Bit(); QByteArray value("1"); const qsizetype eq = name.indexOf('='); if (eq >= 0) { value = name.mid(eq + 1); name = name.left(eq); } if (name.isEmpty()) { error("Missing macro name"); parser.showHelp(1); } Macro macro; macro.symbols = Preprocessor::tokenize(value, 1, Preprocessor::TokenizeDefine); macro.symbols.removeLast(); // remove the EOF symbol pp.macros.insert(name, macro); } const auto undefines = parser.values(undefineOption); for (const QString &arg : undefines) { QByteArray macro = arg.toLocal8Bit(); if (macro.isEmpty()) { error("Missing macro name"); parser.showHelp(1); } pp.macros.remove(macro); } const QStringList noNotesCompatValues = parser.values(noNotesWarningsCompatOption); if (parser.isSet(noNotesOption) || noNotesCompatValues.contains("n"_L1)) moc.displayNotes = false; if (parser.isSet(noWarningsOption) || noNotesCompatValues.contains("w"_L1)) moc.displayWarnings = moc.displayNotes = false; if (autoInclude) { qsizetype spos = filename.lastIndexOf(QDir::separator()); qsizetype ppos = filename.lastIndexOf(u'.'); // spos >= -1 && ppos > spos => ppos >= 0 moc.noInclude = (ppos > spos && filename.at(ppos + 1).toLower() != u'h'); } if (defaultInclude) { if (moc.includePath.isEmpty()) { if (filename.size()) { if (output.size()) moc.includeFiles.append(combinePath(filename, output)); else moc.includeFiles.append(QFile::encodeName(filename)); } } else { moc.includeFiles.append(combinePath(filename, filename)); } } if (filename.isEmpty()) { filename = QStringLiteral("standard input"); if (!in.open(stdin, QIODevice::ReadOnly)) { fprintf(stderr, "moc: cannot open standard input: %s\n", qPrintable(in.errorString())); return 1; } } else { in.setFileName(filename); if (!in.open(QIODevice::ReadOnly)) { fprintf(stderr, "moc: cannot open %s: %s\n", qPrintable(filename), qPrintable(in.errorString())); return 1; } moc.filename = filename.toLocal8Bit(); } const auto metadata = parser.values(metadataOption); for (const QString &md : metadata) { qsizetype split = md.indexOf(u'='); QString key = md.left(split); QString value = md.mid(split + 1); if (split == -1 || key.isEmpty() || value.isEmpty()) { error("missing key or value for option '-M'"); } else if (key.indexOf(u'.') != -1) { // Don't allow keys with '.' for now, since we might need this // format later for more advanced meta data API error("A key cannot contain the letter '.' for option '-M'"); } else { QJsonArray array = moc.metaArgs.value(key); array.append(value); moc.metaArgs.insert(key, array); } } moc.currentFilenames.push(filename.toLocal8Bit()); moc.includes = pp.includes; if (Q_UNLIKELY(parser.isSet(debugIncludesOption))) { fprintf(stderr, "debug-includes: include search list:\n"); for (auto &includePath : pp.includes) { fprintf(stderr, "debug-includes: '%s' framework: %d \n", includePath.path.constData(), includePath.isFrameworkPath); } fprintf(stderr, "debug-includes: end of search list.\n"); } // 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()) { fprintf(stderr, "Warning: Failed to resolve include \"%s\" for moc file %s\n", includeName.toLocal8Bit().constData(), moc.filename.isEmpty() ? "" : moc.filename.constData()); } else { QFile f(QFile::decodeName(rawName)); if (f.open(QIODevice::ReadOnly)) { 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(), moc.filename.isEmpty() ? "" : moc.filename.constData(), f.errorString().toLocal8Bit().constData()); } } } moc.symbols += pp.preprocessed(moc.filename, &in); if (!pp.preprocessOnly) { // 2. parse moc.parse(); } // 3. and output meta object code QScopedPointer jsonOutput; bool outputToFile = true; if (output.size()) { // output file specified #if defined(_MSC_VER) if (_wfopen_s(&out, reinterpret_cast(output.utf16()), L"w") != 0) #else out = fopen(QFile::encodeName(output).constData(), "w"); // create output file if (!out) #endif { const auto fopen_errno = errno; fprintf(stderr, "moc: Cannot create %s. Error: %s\n", QFile::encodeName(output).constData(), strerror(fopen_errno)); return 1; } if (parser.isSet(jsonOption)) { const QString jsonOutputFileName = output + ".json"_L1; FILE *f; #if defined(_MSC_VER) if (_wfopen_s(&f, reinterpret_cast(jsonOutputFileName.utf16()), L"w") != 0) #else f = fopen(QFile::encodeName(jsonOutputFileName).constData(), "w"); if (!f) #endif { const auto fopen_errno = errno; fprintf(stderr, "moc: Cannot create JSON output file %s. Error: %s\n", QFile::encodeName(jsonOutputFileName).constData(), strerror(fopen_errno)); } jsonOutput.reset(f); } } else { // use stdout out = stdout; outputToFile = false; } if (pp.preprocessOnly) { fprintf(out, "%s\n", composePreprocessorOutput(moc.symbols).constData()); } else { if (moc.classList.isEmpty()) moc.note("No relevant classes found. No output generated."); else moc.generate(out, jsonOutput.data()); } 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 + ".d"_L1; } else { fprintf(stderr, "moc: Writing to stdout, but no depfile path specified.\n"); } QScopedPointer depFileHandle; FILE *depFileHandleRaw; #if defined(_MSC_VER) if (_wfopen_s(&depFileHandleRaw, reinterpret_cast(depOutputFileName.utf16()), L"w") != 0) #else depFileHandleRaw = fopen(QFile::encodeName(depOutputFileName).constData(), "w"); if (!depFileHandleRaw) #endif { const auto fopen_errno = errno; fprintf(stderr, "moc: Cannot create dep output file '%s'. Error: %s\n", QFile::encodeName(depOutputFileName).constData(), strerror(fopen_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; } QT_END_NAMESPACE int main(int _argc, char **_argv) { return QT_PREPEND_NAMESPACE(runMoc)(_argc, _argv); }