Doxygen源码分析:doxygen执行过程的拆解

Doxygen源码分析:doxygen执行过程的拆解

2023-05-19 23:09:17 ~ 2023-05-20 16:38:13
ChrisZZ imzhuo@foxmailcom
Hompage https://github.com/zchrissirhcz

在这里插入图片描述

1. doxygen 版本

本次使用的 doxygen 版本如下, 相比于前几篇源码分析时用的版本,有更新,对应到 doxygen 正式版1.9.8(没有特别大的变化):

$ git log
commit 5fded4215d4f9271fe92c940fc4532d4704f5be1 (HEAD -> master, upstream/master)
Author: Dimitri van Heesch <doxygen@gmail.com>
Date:   Thu May 18 22:14:30 2023 +0200

    bump version to 1.9.8 for development

2. doxygen 可执行程序的入口: src/main.cpp

在这里插入图片描述

src/CMakeLists.txt 中看到 doxygen 这个 target 的描述, 它由 main.cppdoxygen.rc 两个文件组成:

add_executable(doxygen
    main.cpp
    ${PROJECT_SOURCE_DIR}/templates/icon/doxygen.rc
)

doxygen.rc 不是代码, 忽略; main.cpp 给出了整个 doxygen 执行的6部分内容:

#include "doxygen.h"

/*! \file
 *  \brief main entry point for doxygen
 *
 *  This file contains main()
 */

/*! Default main. The idea of separating this from the rest of doxygen,
 *  is to make it possible to write your own main, with a different
 *  generateOutput() function for instance.
 */
int main(int argc,char **argv)
{
  initDoxygen();                // 初始化 doxygen
  readConfiguration(argc,argv); // 读取配置, 通常是读取 Doxyfile 这一配置文件
  checkConfiguration();         // 检查和解析配置选项
  adjustConfiguration();        // 调整配置:有些全局变量依赖于配置
  parseInput();                 // 解析输入
  generateOutput();             // 生成输出, 例如 html, xml, latex, pdf
  return 0;
}

3. initDoxygen(): 初始化 doxygen 的过程浅析

包括如下部分:

  • 初始化资源:加载 templates 目录下的文件
  • 设置语言、编码等本地化的配置
  • 检查和修正路径
  • 调试工具的计时器开始计时
  • 给各编程语言注册 parser
  • 为 LinkedMap 类的子类实例执行初始化:成员名字、函数名字、namespace、class 等
  • 其他全局配置的初始化

以下是包含注释的 initDoxygen() 函数源码:

// src/doxyge.cpp, L10724-L10803
void initDoxygen()
{
  initResources();  // 初始化资源, 在 build/generated_src/resources.cpp 里实现, 把 templates 目录下的每个文件都注册进来
  QCString lang = Portable::getenv("LC_ALL"); // QCString 是 doxygen 中重新实现的, 接口和 Qt 的 QCString 保持一致。内部使用 std::string.
  if (!lang.isEmpty()) Portable::setenv("LANG",lang); // 设置语言,编码等
  std::setlocale(LC_ALL,"");
  std::setlocale(LC_CTYPE,"C"); // to get isspace(0xA0)==0, needed for UTF-8
  std::setlocale(LC_NUMERIC,"C");

  Doxygen::symbolMap = new SymbolMap<Definition>; // SymbolMap 是模板类, 把符号名字映射为对象。 Definition 是所有定义实体(类,成员函数,文件,作用域等)的公共基类

  Portable::correct_path(); // Portable 是一个 namespace, 提供了跨平台的函数, 如 sysmtem(), pid,(), getenv(), sleep(), isAbsolutePath() 等。
  // correct_path() 用于修正一个可能错误的路径,例如在 MSVC 编译器下, 把路径中的 "/" 替换为 "\\"

  Debug::startTimer(); // 开启全局计时器。 Debug 是一个类, 包含了打印函数 print(), 用于调试。

  // ParserManger 管理各种编程语言的解析器,提供注册 parser、根据文件扩展获取 parser 的功能。
  // 默认注册的语言是: C, Python, Fortran, FortranFree, FortranFixed, VHDL, XML, SQL, Markdown, Lex
  Doxygen::parserManager = new ParserManager(            make_parser_factory<NullOutlineParser>(), 
                                                         make_parser_factory<FileCodeParser>());
  Doxygen::parserManager->registerParser("c",            make_parser_factory<COutlineParser>(),
                                                         make_parser_factory<CCodeParser>());
  Doxygen::parserManager->registerParser("python",       make_parser_factory<PythonOutlineParser>(),
                                                         make_parser_factory<PythonCodeParser>());
  Doxygen::parserManager->registerParser("fortran",      make_parser_factory<FortranOutlineParser>(),
                                                         make_parser_factory<FortranCodeParser>());
  Doxygen::parserManager->registerParser("fortranfree",  make_parser_factory<FortranOutlineParserFree>(),
                                                         make_parser_factory<FortranCodeParserFree>());
  Doxygen::parserManager->registerParser("fortranfixed", make_parser_factory<FortranOutlineParserFixed>(),
                                                         make_parser_factory<FortranCodeParserFixed>());
  Doxygen::parserManager->registerParser("vhdl",         make_parser_factory<VHDLOutlineParser>(),
                                                         make_parser_factory<VHDLCodeParser>());
  Doxygen::parserManager->registerParser("xml",          make_parser_factory<NullOutlineParser>(),
                                                         make_parser_factory<XMLCodeParser>());
  Doxygen::parserManager->registerParser("sql",          make_parser_factory<NullOutlineParser>(),
                                                         make_parser_factory<SQLCodeParser>());
  Doxygen::parserManager->registerParser("md",           make_parser_factory<MarkdownOutlineParser>(),
                                                         make_parser_factory<FileCodeParser>());
  Doxygen::parserManager->registerParser("lex",          make_parser_factory<LexOutlineParser>(),
                                                         make_parser_factory<LexCodeParser>());

  // register any additional parsers here...
  // 初始化默认的扩展映射: 把文件的扩展名(如 .hpp) 映射到 parser(如parser id 为 "c")
  initDefaultExtensionMapping();

  // doxygen 默认没开启 libclang. 用户可手动开启, 开启后会使用 clang parser 以获得更准确的解析结果, 不过速度会慢
  // USE_LIBCLANG 适合大量使用了 C++ 模板的代码(的工程中的文档生成), doxygen 内置的 parser 对必要的类型信息比较缺乏。
#if USE_LIBCLANG
  Doxygen::clangUsrMap   = new ClangUsrMap;
#endif
  Doxygen::memberNameLinkedMap = new MemberNameLinkedMap; // MemberNameLinkedMap 类是 “成员名字对象”的有序字典类. 是 LinkedMap<MemberName>
  Doxygen::functionNameLinkedMap = new MemberNameLinkedMap; // MemberNameLinkedMap 类是 LinkedMap<MemberName>
  Doxygen::groupLinkedMap = new GroupLinkedMap;  // GroupLinkedMap 类是 LinkedMap<GroupDef>
  Doxygen::namespaceLinkedMap = new NamespaceLinkedMap; // NamespaceLinkedMap 类是 LinkedMap<NamespaceDef>
  Doxygen::classLinkedMap = new ClassLinkedMap; // ClassLinkedMap 类是 LinkedMap<ClassDef>
  Doxygen::hiddenClassLinkedMap = new ClassLinkedMap;
  Doxygen::conceptLinkedMap = new ConceptLinkedMap; // ConceptLinkedMap 类是 LinkedMap<ConceptDef>
  Doxygen::dirLinkedMap = new DirLinkedMap;         //  DirLinkedMap 类是 LinkedMap<DirDef>
  Doxygen::pageLinkedMap = new PageLinkedMap;          // all doc pages  // PageLinkedMap 类是 LinkedMap<PageDef>
  Doxygen::exampleLinkedMap = new PageLinkedMap;       // all examples
  //Doxygen::tagDestinationDict.setAutoDelete(TRUE);
  Doxygen::indexList = new IndexList;           // 索引接口列表类

  // initialisation of these globals depends on
  // configuration switches so we need to postpone these
  // 如下全局变量的值,可以在配置文件中覆盖,此处仅做默认值的赋值
  // 实际上它们都是空指针, 使用 nullptr 替代 0 更合适
  Doxygen::globalScope     = 0;
  Doxygen::inputNameLinkedMap   = 0;
  Doxygen::includeNameLinkedMap = 0;
  Doxygen::exampleNameLinkedMap = 0;
  Doxygen::imageNameLinkedMap   = 0;
  Doxygen::dotFileNameLinkedMap = 0;
  Doxygen::mscFileNameLinkedMap = 0;
  Doxygen::diaFileNameLinkedMap = 0;

  /**************************************************************************
   *            Initialize some global constants
   **************************************************************************/
  // g_compoundKeywords 是一个 set, 基本元素是 string
  g_compoundKeywords.insert("template class");
  g_compoundKeywords.insert("template struct");
  g_compoundKeywords.insert("class");
  g_compoundKeywords.insert("struct");
  g_compoundKeywords.insert("union");
  g_compoundKeywords.insert("interface");
  g_compoundKeywords.insert("exception");
}

4. readConfiguration(argc, argv): 读取配置文件的过程浅析

  • 获取 doxygen 版本信息
  • 定义 writeFile() 函数
  • 解析 argv 参数

直接看这个函数的源码, 不是很容易连贯起来, 可以直接从 doxygen --help 给出的整段提示内容得到启发,再结合源码可以得到更好的理解:

zz@Legion-R7000P% doxygen --help
Doxygen version 1.9.8 (5fded4215d4f9271fe92c940fc4532d4704f5be1*)
Copyright Dimitri van Heesch 1997-2021

You can use doxygen in a number of ways:

1) Use doxygen to generate a template configuration file*:
    doxygen [-s] -g [configName]

2) Use doxygen to update an old configuration file*:
    doxygen [-s] -u [configName]

3) Use doxygen to generate documentation using an existing configuration file*:
    doxygen [configName]

4) Use doxygen to generate a template file controlling the layout of the
   generated documentation:
    doxygen -l [layoutFileName]

    In case layoutFileName is omitted DoxygenLayout.xml will be used as filename.
    If - is used for layoutFileName doxygen will write to standard output.

5) Use doxygen to generate a template style sheet file for RTF, HTML or Latex.
    RTF:        doxygen -w rtf styleSheetFile
    HTML:       doxygen -w html headerFile footerFile styleSheetFile [configFile]
    LaTeX:      doxygen -w latex headerFile footerFile styleSheetFile [configFile]

6) Use doxygen to generate a rtf extensions file
    doxygen -e rtf extensionsFile

    If - is used for extensionsFile doxygen will write to standard output.

7) Use doxygen to compare the used configuration file with the template configuration file
    doxygen -x [configFile]

   Use doxygen to compare the used configuration file with the template configuration file
   without replacing the environment variables or CMake type replacement variables
    doxygen -x_noenv [configFile]

8) Use doxygen to show a list of built-in emojis.
    doxygen -f emoji outputFileName

    If - is used for outputFileName doxygen will write to standard output.

*) If -s is specified the comments of the configuration items in the config file will be omitted.
   If configName is omitted 'Doxyfile' will be used as a default.
   If - is used for configFile doxygen will write / read the configuration to /from standard output / input.

If -q is used for a doxygen documentation run, doxygen will see this as if QUIET=YES has been set.

-v print version string, -V print extended version information
-h,-? prints usage help information
doxygen -d prints additional usage flags for debugging purposes

带注释的 readConfiguration(argc, argv) 代码如下:

// src/doxygen.cpp, L10847-L11248
void readConfiguration(int argc, char **argv)
{
  // getFullVersion() 在 libversion/fullversion.cpp 中实现, 结果形如 1.9.8 (git commit id)
  // 其中获取 git commit id 是在 build/generated_src/gitversion.cpp 的 getGitVersion() 函数实现的
  QCString versionString = getFullVersion();

  // helper that calls \a func to write to file \a fileName via a TextStream
  // writeFile 是一个 把 TextStream 写入文件的 lambda 函数。
  // TextStream 类是 std::ostringstream 类的改进版, 更简单,性能也更高
  auto writeFile = [](const char *fileName,std::function<void(TextStream&)> func) -> bool
  {
    std::ofstream f;
    if (openOutputFile(fileName,f))
    {
      TextStream t(&f);
      func(t);
      return true;
    }
    return false;
  };


  /**************************************************************************
   *             Handle arguments                                           *
   **************************************************************************/
  // 解析 argv[1]...argv[argc-1] 参数。对于每个 argv[i] 来说, 需要 argv[i][0] 为 '-', argv[i][1] 为具体的选项(单个字母)
  // 例如, doxygen -g   其中 g 表示要生成(generate)配置文件
  int optInd=1;
  QCString configName;
  QCString layoutName;
  QCString debugLabel;
  QCString formatName;
  QCString listName;
  QCString traceName;
  bool genConfig=FALSE;
  bool shortList=FALSE;
  Config::CompareMode diffList=Config::CompareMode::Full;
  bool updateConfig=FALSE;
  int retVal;
  bool quiet = false;
  while (optInd<argc && argv[optInd][0]=='-' &&
               (isalpha(argv[optInd][1]) || argv[optInd][1]=='?' ||
                argv[optInd][1]=='-')
        )
  {
    switch(argv[optInd][1])
    {
      case 'g': // g = generate, 生成配置文件
        genConfig=TRUE;
        break;
      case 'l': // l = layout, 指定 xml 布局文件
        if (optInd+1>=argc)
        {
          layoutName="DoxygenLayout.xml";
        }
        else
        {
          layoutName=argv[optInd+1];
        }
        writeDefaultLayoutFile(layoutName);
        cleanUpDoxygen();
        exit(0);
        break;
      case 'd': // d = debug, 生成调试标签。给 doxygen 开发者用的。
        debugLabel=getArg(argc,argv,optInd);
        if (debugLabel.isEmpty())
        {
          devUsage();
          cleanUpDoxygen();
          exit(0);
        }
        retVal = Debug::setFlagStr(debugLabel);
        if (!retVal)
        {
          err("option \"-d\" has unknown debug specifier: \"%s\".\n",qPrint(debugLabel));
          devUsage();
          cleanUpDoxygen();
          exit(1);
        }
        break;
      case 't': // t = tracing, 开启开发 log
        {
#if ENABLE_TRACING
          if (optInd+1>=argc || argv[optInd+1][0] == '-')
          {
            traceName="trace.txt";
          }
          else
          {
            traceName=argv[optInd+1];
            optInd++;
          }
#else
          err("support for option \"-t\" has not been compiled in (use a debug build or a release build with tracing enabled).\n");
          cleanUpDoxygen();
          exit(1);
#endif
        }
        break;
      case 'x': // x = compare,意思是比较当前实用的配置文件和模板中的配置文件。类型定义位于 src/config.h 中的 config::CompareMode 类。
        if (!strcmp(argv[optInd]+1,"x_noenv")) diffList=Config::CompareMode::CompressedNoEnv;
        else if (!strcmp(argv[optInd]+1,"x")) diffList=Config::CompareMode::Compressed;
        else
        {
          err("option should be \"-x\" or \"-x_noenv\", found: \"%s\".\n",argv[optInd]);
          cleanUpDoxygen();
          exit(1);
        }
        break;
      case 's': // s = shortlist, 含义暂时不明
        shortList=TRUE;
        break;
      case 'u': // u = update config, 更新配置文件
        updateConfig=TRUE;
        break;
      case 'e': // e = extension, 用于生成 RTF 扩展文件, 例如 doxygen -e rtf extensionsFile
        formatName=getArg(argc,argv,optInd);
        if (formatName.isEmpty())
        {
          err("option \"-e\" is missing format specifier rtf.\n");
          cleanUpDoxygen();
          exit(1);
        }
        if (qstricmp(formatName.data(),"rtf")==0)
        {
          if (optInd+1>=argc)
          {
            err("option \"-e rtf\" is missing an extensions file name\n");
            cleanUpDoxygen();
            exit(1);
          }
          writeFile(argv[optInd+1],RTFGenerator::writeExtensionsFile);
          cleanUpDoxygen();
          exit(0);
        }
        err("option \"-e\" has invalid format specifier.\n");
        cleanUpDoxygen();
        exit(1);
        break;
      case 'f': // f = filename, 显示 doxygen 内置的 emoji, 并打印输出到文件 
        listName=getArg(argc,argv,optInd);
        if (listName.isEmpty())
        {
          err("option \"-f\" is missing list specifier.\n");
          cleanUpDoxygen();
          exit(1);
        }
        if (qstricmp(listName.data(),"emoji")==0)
        {
          if (optInd+1>=argc)
          {
            err("option \"-f emoji\" is missing an output file name\n");
            cleanUpDoxygen();
            exit(1);
          }
          writeFile(argv[optInd+1],[](TextStream &t) { EmojiEntityMapper::instance().writeEmojiFile(t); });
          cleanUpDoxygen();
          exit(0);
        }
        err("option \"-f\" has invalid list specifier.\n");
        cleanUpDoxygen();
        exit(1);
        break;
      case 'w': // w = ? , 让 doxygen 为 RTF, HTML or Latex 输出生成样式文件
        formatName=getArg(argc,argv,optInd);
        if (formatName.isEmpty())
        {
          err("option \"-w\" is missing format specifier rtf, html or latex\n");
          cleanUpDoxygen();
          exit(1);
        }
        if (qstricmp(formatName.data(),"rtf")==0)
        {
          if (optInd+1>=argc)
          {
            err("option \"-w rtf\" is missing a style sheet file name\n");
            cleanUpDoxygen();
            exit(1);
          }
          if (!writeFile(argv[optInd+1],RTFGenerator::writeStyleSheetFile))
          {
            err("error opening RTF style sheet file %s!\n",argv[optInd+1]);
            cleanUpDoxygen();
            exit(1);
          }
          cleanUpDoxygen();
          exit(0);
        }
        else if (qstricmp(formatName.data(),"html")==0)
        {
          Config::init();
          if (optInd+4<argc || FileInfo("Doxyfile").exists())
             // explicit config file mentioned or default found on disk
          {
            QCString df = optInd+4<argc ? argv[optInd+4] : QCString("Doxyfile");
            if (!Config::parse(df)) // parse the config file
            {
              err("error opening or reading configuration file %s!\n",argv[optInd+4]);
              cleanUpDoxygen();
              exit(1);
            }
          }
          if (optInd+3>=argc)
          {
            err("option \"-w html\" does not have enough arguments\n");
            cleanUpDoxygen();
            exit(1);
          }
          Config::postProcess(TRUE);
          Config::updateObsolete();
          Config::checkAndCorrect(Config_getBool(QUIET), false);
          setTranslator(Config_getEnum(OUTPUT_LANGUAGE));
          writeFile(argv[optInd+1],[&](TextStream &t) { HtmlGenerator::writeHeaderFile(t,argv[optInd+3]); });
          writeFile(argv[optInd+2],HtmlGenerator::writeFooterFile);
          writeFile(argv[optInd+3],HtmlGenerator::writeStyleSheetFile);
          cleanUpDoxygen();
          exit(0);
        }
        else if (qstricmp(formatName.data(),"latex")==0)
        {
          Config::init();
          if (optInd+4<argc || FileInfo("Doxyfile").exists())
          {
            QCString df = optInd+4<argc ? argv[optInd+4] : QCString("Doxyfile");
            if (!Config::parse(df))
            {
              err("error opening or reading configuration file %s!\n",argv[optInd+4]);
              cleanUpDoxygen();
              exit(1);
            }
          }
          if (optInd+3>=argc)
          {
            err("option \"-w latex\" does not have enough arguments\n");
            cleanUpDoxygen();
            exit(1);
          }
          Config::postProcess(TRUE);
          Config::updateObsolete();
          Config::checkAndCorrect(Config_getBool(QUIET), false);
          setTranslator(Config_getEnum(OUTPUT_LANGUAGE));
          writeFile(argv[optInd+1],LatexGenerator::writeHeaderFile);
          writeFile(argv[optInd+2],LatexGenerator::writeFooterFile);
          writeFile(argv[optInd+3],LatexGenerator::writeStyleSheetFile);
          cleanUpDoxygen();
          exit(0);
        }
        else
        {
          err("Illegal format specifier \"%s\": should be one of rtf, html or latex\n",qPrint(formatName));
          cleanUpDoxygen();
          exit(1);
        }
        break;
      case 'm':  // m = map, 导出符号映射文件
        g_dumpSymbolMap = TRUE;
        break;
      case 'v': // v = version, 用 msg() 打印版本信息, false 表示不要打印扩展的版本信息内容(如sqlite, libclang等)
        version(false);
        cleanUpDoxygen();
        exit(0);
        break;
      case 'V': // V = version, 打印常规的版本信息之外, 还打印扩展的版本信息内容
        version(true);
        cleanUpDoxygen();
        exit(0);
        break;
      case '-': 
        if (qstrcmp(&argv[optInd][2],"help")==0) // --help 打印帮助信息
        {
          usage(argv[0],versionString);
          exit(0);
        }
        else if (qstrcmp(&argv[optInd][2],"version")==0) // --version 打印版本信息
        {
          version(false);
          cleanUpDoxygen();
          exit(0);
        }
        else if ((qstrcmp(&argv[optInd][2],"Version")==0) ||
                 (qstrcmp(&argv[optInd][2],"VERSION")==0))  // --Version 和 --VERSION 打印版本信息
        {
          version(true);
          cleanUpDoxygen();
          exit(0);
        }
        else
        {
          err("Unknown option \"-%s\"\n",&argv[optInd][1]);
          usage(argv[0],versionString);
          exit(1);
        }
        break;
      case 'b': // b = buf, 指定IO流的缓冲区群, `_IONBF` 意思是“不使用缓冲区”,因此会立即输出到控制台
        setvbuf(stdout,NULL,_IONBF,0);
        break;
      case 'q': // q = quiet, 安静模式
        quiet = true;
        break;
      case 'T': // T = template, 意思是使用 Django 风格的模板文件。目前还处于实验状态, 请谨慎使用。
        msg("Warning: this option activates output generation via Django like template files. "
            "This option is scheduled for doxygen 2.0, is currently incomplete and highly experimental! "
            "Only use if you are a doxygen developer\n");
        g_useOutputTemplate=TRUE;
        break;
      case 'h': // -h 和 -? : 打印用法
      case '?':
        usage(argv[0],versionString);
        exit(0);
        break;
      default:
        err("Unknown option \"-%c\"\n",argv[optInd][1]);
        usage(argv[0],versionString);
        exit(1);
    }
    optInd++;
  }

  /**************************************************************************
   *            Parse or generate the config file                           *
   **************************************************************************/
  // 解析或生成配置文件
  initTracing(traceName.data());
  TRACE("Doxygen version used: {}",getFullVersion());
  Config::init();

  FileInfo configFileInfo1("Doxyfile"),configFileInfo2("doxyfile");
  if (optInd>=argc)
  {
    if (configFileInfo1.exists())
    {
      configName="Doxyfile";
    }
    else if (configFileInfo2.exists())
    {
      configName="doxyfile";
    }
    else if (genConfig)
    {
      configName="Doxyfile";
    }
    else
    {
      err("Doxyfile not found and no input file specified!\n");
      usage(argv[0],versionString);
      exit(1);
    }
  }
  else
  {
    FileInfo fi(argv[optInd]);
    if (fi.exists() || qstrcmp(argv[optInd],"-")==0 || genConfig)
    {
      configName=argv[optInd];
    }
    else
    {
      err("configuration file %s not found!\n",argv[optInd]);
      usage(argv[0],versionString);
      exit(1);
    }
  }

  if (genConfig && g_useOutputTemplate)
  {
    generateTemplateFiles("templates");
    cleanUpDoxygen();
    exit(0);
  }

  if (genConfig)
  {
    generateConfigFile(configName,shortList);
    cleanUpDoxygen();
    exit(0);
  }

  if (!Config::parse(configName,updateConfig,diffList))
  {
    err("could not open or read configuration file %s!\n",qPrint(configName));
    cleanUpDoxygen();
    exit(1);
  }

  if (diffList!=Config::CompareMode::Full)
  {
    Config::updateObsolete();
    compareDoxyfile(diffList);
    cleanUpDoxygen();
    exit(0);
  }

  if (updateConfig)
  {
    Config::updateObsolete();
    generateConfigFile(configName,shortList,TRUE);
    cleanUpDoxygen();
    exit(0);
  }

  /* Perlmod wants to know the path to the config file.*/
  FileInfo configFileInfo(configName.str());
  setPerlModDoxyfile(configFileInfo.absFilePath());

  /* handle -q option */
  if (quiet) Config_updateBool(QUIET,TRUE);
}

查看 doxygen 内置的 emoji

doxygen -f emoji doxygen-emoji.md

然后用 VSCode 的 markdown 预览查看:
在这里插入图片描述

开启 Tracing

需要在 cmake 构建阶段传入参数 -D enable_tracing, 或指定构建类型-D CMAKE_BUILD_TYPE=Debug, 这样可以开启 ENABLE_TRACING 宏定义。e.g.

cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/home/zz/soft/doxygen -Denable_tracing=ON
cmake --build build -j14
cmake --build build -j14 --target install

还需要在执行 doxygen 时, 传入 -t 参数:

# 假设当前路径下已经有 Doxyfile 了
# 如下是正确的三种调用方式
doxygen -t          # 会开启 trace, 并记录在名为 trace.txt 的文件中
doxygen -t mytrace.txt # 会开启 trace, 并记录在名为 trace.txt 的文件中
doxygen -t mytrace.txt Doxyfile # 会开启 trace, 并记录在名为 mytrace.txt 的文件中

# 如下是错误的三种调用方式
doxygen             # 不会开启 trace
doxygen Doxyfile    # 不会开启 trace
doxygen Doxxyfile -t mytrace.txt # 不会开启 trace
doxygen -t Doxyfile # 错误:会把 trace 内容输出到 Doxyfile 文件中,覆盖原有内容

5. checkConfiguration(): 检查和解析配置选项的过程浅析

代码只有10行,

/** check and resolve config options */
void checkConfiguration()
{
  AUTO_TRACE(); // 如果开启了 tracing 功能, 会执行 trace

  Config::postProcess(FALSE); // 配置文件的后处理, 如默认值的填充等
  Config::updateObsolete();   // 更新淘汰的配置
  Config::checkAndCorrect(Config_getBool(QUIET), true); // 检查和纠正配置
  initWarningFormat(); // 初始化警告格式
}

6. adjustConfiguration(): 调整配置文件的过程浅析

  • 设置输出文档的自然语言
  • 设置输出 html 文件的后缀名字
  • 判断是否需要额外的 parse , 用于生成额外的调用依赖关系图
  • 检查和处理指定的特殊 parser 映射关系(文件扩展名对应的 parser)
  • 检查全局的输入文件编码配置项目, 以及单个文件级别的文件编码配置项
  • 读取用户定义的宏、别名
  • 读取制表符的宽度
/** adjust globals that depend on configuration settings. */
void adjustConfiguration()
{
  AUTO_TRACE(); // 开启自动 tracing。 取决于编译时是否开启trace, 以及运行 doxygen 时是否传入 -t 参数
  // 一些全局对象的创建。比较奇怪的是,为什么有些用 unique_ptr, 有些则还是 raw pointer.
  Doxygen::globalNamespaceDef = createNamespaceDef("<globalScope>",1,1,"<globalScope>");
  Doxygen::globalScope = toNamespaceDefMutable(Doxygen::globalNamespaceDef.get());
  Doxygen::inputNameLinkedMap = new FileNameLinkedMap;
  Doxygen::includeNameLinkedMap = new FileNameLinkedMap;
  Doxygen::exampleNameLinkedMap = new FileNameLinkedMap;
  Doxygen::imageNameLinkedMap = new FileNameLinkedMap;
  Doxygen::dotFileNameLinkedMap = new FileNameLinkedMap;
  Doxygen::mscFileNameLinkedMap = new FileNameLinkedMap;
  Doxygen::diaFileNameLinkedMap = new FileNameLinkedMap;

  // 设置翻译器。取决于输出的语言。
  // 例如对于中文:     case OUTPUT_LANGUAGE_t::Chinese:             theTranslator = new TranslatorChinese; break;
  setTranslator(Config_getEnum(OUTPUT_LANGUAGE));

  /* Set the global html file extension. */
  // 设置全局 html 文件扩展. 默认使用 .html, 也可以改为 .htm, .php, .asp 等
  // https://www.doxygen.nl/manual/config.html#cfg_extension_mapping
  Doxygen::htmlFileExtension = Config_getString(HTML_FILE_EXTENSION);

  // 是否需要解析源代码: 需要这4个配置中至少有一个为真: CALL_GRAPH, CALLER_GRAPH, REFERENCES_RELATION, REFERENCED_BY_RELATION
  // CALL_GRAPH: https://www.doxygen.nl/manual/config.html#cfg_call_graph
  //             如果为真, 则doxygen将为每个全局函数或类方法生成一个调用依赖图。
  //             启用此选项将显著增加运行时间。因此,在大多数情况下,最好只使用\callgraph命令为所选函数启用调用图。禁用调用图可以通过命令\hidecallgraph来完成。
  // CALLER_GRAPH: https://www.doxygen.nl/manual/config.html#cfg_caller_graph
  //             如果为真, 则为每个全局函数和类方法生成“谁调用了它”的依赖关系图
  // REFERENCES_RELATION: https://www.doxygen.nl/manual/config.html#cfg_references_relation
  //             如果为真, 则为每个被文档化的函数, 列出调用了的、使用了的函数 。(列出“它调用了谁”)
  // REFERENCED_BY_RELATION: https://www.doxygen.nl/manual/config.html#cfg_referenced_by_relation
  //             如果为真, 则对于每个文档化实体,将列出引用该实体的所有文档化函数。 (列出“谁调用了它”)
  Doxygen::parseSourcesNeeded = Config_getBool(CALL_GRAPH) ||
                                Config_getBool(CALLER_GRAPH) ||
                                Config_getBool(REFERENCES_RELATION) ||
                                Config_getBool(REFERENCED_BY_RELATION);

  /**************************************************************************
   *            Add custom extension mappings
   **************************************************************************/
  // 对于每一种扩展的映射的处理
  // doxygen 内置了每一种扩展对应的 parser, 而使用 EXTENSION_MAPPING 则可以覆盖默认的 parser 的 mapping。
  // https://www.doxygen.nl/manual/config.html#cfg_extension_mapping
  const StringVector &extMaps = Config_getList(EXTENSION_MAPPING);
  for (const auto &mapping : extMaps)
  {
    QCString mapStr = mapping.c_str();
    int i=mapStr.find('=');
    if (i==-1)
    {
      continue;
    }
    else
    {
      QCString ext = mapStr.left(i).stripWhiteSpace().lower();
      QCString language = mapStr.mid(i+1).stripWhiteSpace().lower();
      if (ext.isEmpty() || language.isEmpty())
      {
        continue;
      }

      if (!updateLanguageMapping(ext,language))
      {
        err("Failed to map file extension '%s' to unsupported language '%s'.\n"
            "Check the EXTENSION_MAPPING setting in the config file.\n",
            qPrint(ext),qPrint(language));
      }
      else
      {
        msg("Adding custom extension mapping: '%s' will be treated as language '%s'\n",
            qPrint(ext),qPrint(language));
      }
    }
  }
  // create input file exncodings

  // check INPUT_ENCODING
  // 检查输入的文件编码。 用到了 iconv 库, 基于 iconv 的 API 封装实现了 portable_iconv_open()
  // https://www.doxygen.nl/manual/config.html#cfg_input_encoding
  void *cd = portable_iconv_open("UTF-8",Config_getString(INPUT_ENCODING).data());
  if (cd==reinterpret_cast<void *>(-1))
  {
    term("unsupported character conversion: '%s'->'%s': %s\n"
        "Check the 'INPUT_ENCODING' setting in the config file!\n",
        qPrint(Config_getString(INPUT_ENCODING)),qPrint("UTF-8"),strerror(errno));
  }
  else
  {
    portable_iconv_close(cd);
  }

  // check and split INPUT_FILE_ENCODING
  // 拆解 INPUT_FILE_ENCODING 配置项的取值. INPUT_FILE_ENCODING 允许为单个文件设置编码格式
  // https://www.doxygen.nl/manual/config.html#cfg_input_file_encoding
  const StringVector &fileEncod = Config_getList(INPUT_FILE_ENCODING);
  for (const auto &mapping : fileEncod)
  {
    QCString mapStr = mapping.c_str();
    int i=mapStr.find('=');
    // 如果没有找到等号, 说明没给出配置项的取值。
    if (i==-1)
    {
      continue;
    }
    else
    {
      QCString pattern = mapStr.left(i).stripWhiteSpace().lower();
      QCString encoding = mapStr.mid(i+1).stripWhiteSpace().lower();
      if (pattern.isEmpty() || encoding.isEmpty())
      {
        continue;
      }
      cd = portable_iconv_open("UTF-8",encoding.data());
      if (cd==reinterpret_cast<void *>(-1))
      {
        term("unsupported character conversion: '%s'->'%s': %s\n"
            "Check the 'INPUT_FILE_ENCODING' setting in the config file!\n",
            qPrint(encoding),qPrint("UTF-8"),strerror(errno));
      }
      else
      {
        portable_iconv_close(cd);
      }

      Doxygen::inputFileEncodingList.push_back(InputFileEncoding(pattern, encoding));
    }
  }

  // add predefined macro name to a dictionary
  // 添加预定义的宏定义到字典中
  // https://www.doxygen.nl/manual/config.html#cfg_expand_as_defined
  const StringVector &expandAsDefinedList =Config_getList(EXPAND_AS_DEFINED);
  for (const auto &s : expandAsDefinedList)
  {
    Doxygen::expandAsDefinedSet.insert(s.c_str());
  }

  // read aliases and store them in a dictionary
  // 读取别名, 并存储到字典中
  // https://www.doxygen.nl/manual/config.html#cfg_aliases
  readAliases();

  // store number of spaces in a tab into Doxygen::spaces
  // 读取 tab 制表符的宽度. 默认是4
  // https://www.doxygen.nl/manual/config.html#cfg_tab_size
  int tabSize = Config_getInt(TAB_SIZE);
  Doxygen::spaces.resize(tabSize+1);
  int sp;for (sp=0;sp<tabSize;sp++) Doxygen::spaces.at(sp)=' ';
  Doxygen::spaces.at(tabSize)='\0';
}

7. parseInput(): 解析输入的过程浅析

函数本体大约600行代码, 考虑到调用的一些函数的代码, 总计超过1000行。过程较复杂,包含的内容很多。

// src/doxygen.cpp, L11727-L12348
void parseInput()
{
  AUTO_TRACE();
  std::atexit(exitDoxygen); // atexit 是注册整个程序运行结束(main()之后)要执行的函数。如果doxygen运行中出错提前结束,并且 DB 文件(filterDBFileNmae)不为空, 则删除这个文件

  // 检查是否配置了使用 clang parser。默认是 False。 如果要开启, 还需要编译 doxygen 时传入 -Duse_libclang=ON
  // https://www.doxygen.nl/manual/config.html#cfg_clang_assisted_parsing
#if USE_LIBCLANG
  Doxygen::clangAssistedParsing = Config_getBool(CLANG_ASSISTED_PARSING);
#endif

  // we would like to show the versionString earlier, but we first have to handle the configuration file
  // to know the value of the QUIET setting.
  QCString versionString = getFullVersion(); // 获取完整的版本字符串
  msg("Doxygen version used: %s\n",qPrint(versionString));

  // DOT_PATH 配置项, 给出了 dot 命令所在的目录
  // https://www.doxygen.nl/manual/config.html#cfg_DOT_PATH
  // computeVerifiedDotPath() 根据给出的 DOT_PATH 的取值作为目录, 拼接得到 dot 命令的路径。如果是 windows 还会添加 .exe 后缀
  computeVerifiedDotPath();

  /**************************************************************************
   *            Make sure the output directory exists
   **************************************************************************/
  // OUTPUT_DIRECTORY: https://www.doxygen.nl/manual/config.html#cfg_output_directory
  // 解析用户指定的文档输出路径。支持相对路径。如果留空则用当前目录。
  QCString outputDirectory = Config_getString(OUTPUT_DIRECTORY);
  if (outputDirectory.isEmpty())
  {
    outputDirectory = Config_updateString(OUTPUT_DIRECTORY,Dir::currentDirPath().c_str());
  }
  else
  {
    Dir dir(outputDirectory.str());
    if (!dir.exists())
    {
      dir.setPath(Dir::currentDirPath());
      if (!dir.mkdir(outputDirectory.str()))
      {
        err("tag OUTPUT_DIRECTORY: Output directory '%s' does not "
            "exist and cannot be created\n",qPrint(outputDirectory));
        cleanUpDoxygen();
        exit(1);
      }
      else
      {
        msg("Notice: Output directory '%s' does not exist. "
            "I have created it for you.\n", qPrint(outputDirectory));
      }
      dir.setPath(outputDirectory.str());
    }
    outputDirectory = Config_updateString(OUTPUT_DIRECTORY,dir.absPath().c_str());
  }
  AUTO_TRACE_ADD("outputDirectory={}",outputDirectory);

  /**************************************************************************
   *            Initialize global lists and dictionaries
   **************************************************************************/
  // 初始化全局的列表和字典

  // 可以使用lookup_cache_size设置符号查找缓存的大小。此缓存用于解析给定名称和范围的符号。
  // also scale lookup cache with SYMBOL_CACHE_SIZE
  // https://www.doxygen.nl/manual/config.html#cfg_lookup_cache_size
  int cacheSize = Config_getInt(LOOKUP_CACHE_SIZE);
  if (cacheSize<0) cacheSize=0;
  if (cacheSize>9) cacheSize=9;
  uint32_t lookupSize = 65536 << cacheSize;
  Doxygen::typeLookupCache = new Cache<std::string,LookupInfo>(lookupSize);
  Doxygen::symbolLookupCache = new Cache<std::string,LookupInfo>(lookupSize);

#ifdef HAS_SIGNALS
  signal(SIGINT, stopDoxygen);
#endif

  // 获取当前进程的 ID, 用于 filterdb 文件的命名
  uint32_t pid = Portable::pid();
  Doxygen::filterDBFileName.sprintf("doxygen_filterdb_%d.tmp",pid);
  Doxygen::filterDBFileName.prepend(outputDirectory+"/");

  /**************************************************************************
   *            Check/create output directories                             *
   **************************************************************************/
  // 检查输出目录

  QCString htmlOutput;
  // GENERATE_HTML: https://www.doxygen.nl/manual/config.html#cfg_generate_html
  // 是否输出 html 文件。 默认为 YES。
  bool generateHtml = Config_getBool(GENERATE_HTML);
  if (generateHtml || g_useOutputTemplate /* TODO: temp hack */)
  {
    htmlOutput = createOutputDirectory(outputDirectory,Config_getString(HTML_OUTPUT),"/html");
    Config_updateString(HTML_OUTPUT,htmlOutput);

    // SITEMAP_URL: https://www.doxygen.nl/manual/config.html#cfg_sitemap_url
    // html 文档部署时使用的完整 URL
    QCString sitemapUrl = Config_getString(SITEMAP_URL);
    bool generateSitemap = !sitemapUrl.isEmpty();
    if (generateSitemap && !sitemapUrl.endsWith("/"))
    {
      Config_updateString(SITEMAP_URL,sitemapUrl+"/");
    }

    // add HTML indexers that are enabled
    // GENERATE_HTMLHELP: https://www.doxygen.nl/manual/config.html#cfg_generate_htmlhelp 。默认为 NO。
    // 如果为YES,则生成三个额外的文件: index.hhp, index.hhc, and index.hhk
    //   - index.hhp: 生成 HTML Help Project 文件. 在 Windows 系统上,使用 HTML Help Workshop 工具, 选择 hhp 文件和 html 文件,可以生成 .chm 文件
    //                .chm 是  compiled HTML file 缩写,是单个文件.
    bool generateHtmlHelp    = Config_getBool(GENERATE_HTMLHELP);

    // GENERATE_ECLIPSEHELP: https://www.doxygen.nl/manual/config.html#cfg_generate_eclipsehelp
    // 用于生成 eclipse 里的文档页面
    bool generateEclipseHelp = Config_getBool(GENERATE_ECLIPSEHELP);

    // GENERATE_QHP: https://www.doxygen.nl/manual/config.html#cfg_generate_qhp
    // 用于生成 Qt 里的文档页面
    bool generateQhp         = Config_getBool(GENERATE_QHP);

    // GENERATE_TREEVIEW: https://www.doxygen.nl/manual/config.html#cfg_generate_treeview
    // GENERATE_TREEVIEW标记用于指定是否应生成树状索引结构来显示分层信息。
    // 如果标记值设置为YES,将生成一个包含树状索引结构的侧面板(就像为HTML帮助生成的一样)。为此,需要一个支持JavaScript、DHTML、CSS和框架的浏览器(即任何现代浏览器)。
    // 
    // 通过自定义样式表(请参阅HTML_EXTRA_STYLESHEET),可以进一步微调索引的外观(请参阅微调输出)。
    // 例如,doxygen生成的默认样式表有一个示例,显示了如何将图像放在树的根而不是PROJECT_NAME。
    // 
    // 由于树基本上具有与选项卡索引相同的信息,因此在启用此选项时,可以考虑将DISABLE_index设置为YES。
    bool generateTreeView    = Config_getBool(GENERATE_TREEVIEW);

    // GENERATE_DOCSET: https://www.doxygen.nl/manual/config.html#cfg_generate_docset
    // 用于生成 XCode 里的文档
    bool generateDocSet      = Config_getBool(GENERATE_DOCSET);

    if (generateEclipseHelp) Doxygen::indexList->addIndex<EclipseHelp>(); // 如果用 Eclipse, 可以开启这个选项。
    if (generateHtmlHelp)    Doxygen::indexList->addIndex<HtmlHelp>(); // 如果配置了, 则生成 index.hhp 文件, 如果打算生成 Windows .chm电子书,则开启它。
    if (generateQhp)         Doxygen::indexList->addIndex<Qhp>();  // 如果配置了, 则生成 qt 的文档支持。 本人不用 Qt, 关闭。
    if (generateSitemap)     Doxygen::indexList->addIndex<Sitemap>(); // 如果配置了, 则生成 sitemap 的支持。 通常对于 html 输出, 应该开启这个。
    if (generateTreeView)    Doxygen::indexList->addIndex<FTVHelp>(TRUE); // 如果配置了, 则添加 treeview 的支持。通常对于 html 输出, 应该开启这个。
    if (generateDocSet)      Doxygen::indexList->addIndex<DocSets>();  // 如果配置了, 则添加 XCode 文档生成的支持
    Doxygen::indexList->initialize(); // 对每个 list item, 执行 dispatch_call(). 详见 src/dispatcher.h
  }

  // GENERATE_DOCBOOK: https://www.doxygen.nl/manual/config.html#cfg_generate_docbook
  // 如果为 YES, 则生成 docbook。 docbook 用于生成 pdf。 docbook 基本上还是用xml写的一些文件。
  QCString docbookOutput;
  bool generateDocbook = Config_getBool(GENERATE_DOCBOOK);
  if (generateDocbook)
  {
    docbookOutput = createOutputDirectory(outputDirectory,Config_getString(DOCBOOK_OUTPUT),"/docbook");
    Config_updateString(DOCBOOK_OUTPUT,docbookOutput);
  }

  // GENERATE_XML: https://www.doxygen.nl/manual/config.html#cfg_generate_xml
  // If the GENERATE_XML tag is set to YES, doxygen will generate an XML file that captures the structure of the code including all documentation. 
  // 不是很熟悉 xml 输出, 看起来 xml 输出更多的是作为结构化结果, 是一种中间表示(IR),而不是最终给用户看的
  QCString xmlOutput;
  bool generateXml = Config_getBool(GENERATE_XML);
  if (generateXml)
  {
    xmlOutput = createOutputDirectory(outputDirectory,Config_getString(XML_OUTPUT),"/xml");
    Config_updateString(XML_OUTPUT,xmlOutput);
  }

  // 生成 latex。
  QCString latexOutput;
  bool generateLatex = Config_getBool(GENERATE_LATEX);
  if (generateLatex)
  {
    latexOutput = createOutputDirectory(outputDirectory,Config_getString(LATEX_OUTPUT), "/latex");
    Config_updateString(LATEX_OUTPUT,latexOutput);
  }

  // 生成 rtf
  QCString rtfOutput;
  bool generateRtf = Config_getBool(GENERATE_RTF);
  if (generateRtf)
  {
    rtfOutput = createOutputDirectory(outputDirectory,Config_getString(RTF_OUTPUT),"/rtf");
    Config_updateString(RTF_OUTPUT,rtfOutput);
  }

  // 生成 man 页面
  QCString manOutput;
  bool generateMan = Config_getBool(GENERATE_MAN);
  if (generateMan)
  {
    manOutput = createOutputDirectory(outputDirectory,Config_getString(MAN_OUTPUT),"/man");
    Config_updateString(MAN_OUTPUT,manOutput);
  }

  // 生成 sqlite3 输出
#if USE_SQLITE3
  QCString sqlOutput;
  bool generateSql = Config_getBool(GENERATE_SQLITE3);
  if (generateSql)
  {
    sqlOutput = createOutputDirectory(outputDirectory,Config_getString(SQLITE3_OUTPUT),"/sqlite3");
    Config_updateString(SQLITE3_OUTPUT,sqlOutput);
  }
#endif

  // 如果安装了 dot 命令, 并且 DOT_FONTPATH 配置项为 YES, 则配置 dot 使用的字体
  if (Config_getBool(HAVE_DOT))
  {
    QCString curFontPath = Config_getString(DOT_FONTPATH);
    if (curFontPath.isEmpty())
    {
      Portable::getenv("DOTFONTPATH");
      QCString newFontPath = ".";
      if (!curFontPath.isEmpty())
      {
        newFontPath+=Portable::pathListSeparator();
        newFontPath+=curFontPath;
      }
      Portable::setenv("DOTFONTPATH",qPrint(newFontPath));
    }
    else
    {
      Portable::setenv("DOTFONTPATH",qPrint(curFontPath));
    }
  }



  /**************************************************************************
   *             Handle layout file                                         *
   **************************************************************************/
  // 处理布局文件. 默认布局文件是 DoxygenLayout.xml
  LayoutDocManager::instance().init();
  QCString layoutFileName = Config_getString(LAYOUT_FILE);
  bool defaultLayoutUsed = FALSE;
  if (layoutFileName.isEmpty())
  {
    layoutFileName = Config_updateString(LAYOUT_FILE,"DoxygenLayout.xml");
    defaultLayoutUsed = TRUE;
  }
  AUTO_TRACE_ADD("defaultLayoutUsed={}, layoutFileName={}",defaultLayoutUsed,layoutFileName);

  FileInfo fi(layoutFileName.str());
  if (fi.exists())
  {
    msg("Parsing layout file %s...\n",qPrint(layoutFileName));
    LayoutDocManager::instance().parse(layoutFileName);
  }
  else if (!defaultLayoutUsed)
  {
      warn_uncond("failed to open layout file '%s' for reading! Using default settings.\n",qPrint(layoutFileName));
  }

  /**************************************************************************
   *             Read and preprocess input                                  *
   **************************************************************************/

  // prevent search in the output directories
  // 对于每一种输出类型(html,latex等), 不要作为生成文档的输入。
  // EXCLUDE_PATTERNS: https://www.doxygen.nl/manual/config.html#cfg_exclude_patterns
  StringVector exclPatterns = Config_getList(EXCLUDE_PATTERNS);
  if (generateHtml)    exclPatterns.push_back(htmlOutput.str());
  if (generateDocbook) exclPatterns.push_back(docbookOutput.str());
  if (generateXml)     exclPatterns.push_back(xmlOutput.str());
  if (generateLatex)   exclPatterns.push_back(latexOutput.str());
  if (generateRtf)     exclPatterns.push_back(rtfOutput.str());
  if (generateMan)     exclPatterns.push_back(manOutput.str());
  Config_updateList(EXCLUDE_PATTERNS,exclPatterns);

  // 搜索输入文件。 包括这几种类型: 
  // INCLUDE_PATH: 包含的文件
  // EXAMPLE_PATH: 文件名字或目录, 存储例子代码。 用于使用 `\include` 命令时将一个文件的内容作为一个block引入
  // IMAGE_PATH: 图像路径。
  // DOTFILE_DIRS:类似前面几个。
  // MSCFILE_DIRS
  // DIAFILE_DIRS
  // EXCLUDE
  searchInputFiles();

  // 检查 markdown 主文件, 用于作为 html 页面的主页
  checkMarkdownMainfile();

  // Notice: the order of the function calls below is very important!
  // 如下几个判断的顺序不能修改
  if (Config_getBool(GENERATE_HTML) && !Config_getBool(USE_MATHJAX))
  {
    FormulaManager::instance().initFromRepository(Config_getString(HTML_OUTPUT));
  }
  if (Config_getBool(GENERATE_RTF))
  {
    FormulaManager::instance().initFromRepository(Config_getString(RTF_OUTPUT));
  }
  if (Config_getBool(GENERATE_DOCBOOK))
  {
    FormulaManager::instance().initFromRepository(Config_getString(DOCBOOK_OUTPUT));
  }

  FormulaManager::instance().checkRepositories();

  /**************************************************************************
   *             Handle Tag Files                                           *
   **************************************************************************/
  // 处理 tag 文件
  std::shared_ptr<Entry> root = std::make_shared<Entry>();
  msg("Reading and parsing tag files\n");

  const StringVector &tagFileList = Config_getList(TAGFILES);
  for (const auto &s : tagFileList)
  {
    readTagFile(root,s.c_str());
  }

  /**************************************************************************
   *             Parse source files                                         *
   **************************************************************************/
  // 处理源代码文件

  // 增加 STL 支持
  addSTLSupport(root);

  g_s.begin("Parsing files\n");
  // doxygen 解析文件时, 默认单线程, 也可以指定多线程的数量, 最大32
  // https://www.doxygen.nl/manual/config.html#cfg_num_proc_threads
  if (Config_getInt(NUM_PROC_THREADS)==1)
  {
    parseFilesSingleThreading(root);
  }
  else
  {
    parseFilesMultiThreading(root);
  }
  g_s.end();

  /**************************************************************************
   *             Gather information                                         *
   **************************************************************************/
  // 收集信息. g_s 是 Statistics 类的实例。
  g_s.begin("Building macro definition list...\n");
  // 宏定义: 把预处理阶段扫描出的宏定义, 作为文件成员
  buildDefineList();
  g_s.end();

  g_s.begin("Building group list...\n");
  // group 的处理。也就是被提取文档的 C/C++ 代码中的 @defgroups, @addtogroup, @weakgroup 区块中的group
  buildGroupList(root.get());
  organizeSubGroups(root.get());
  g_s.end();

  g_s.begin("Building directory list...\n");
  // 目录的处理
  buildDirectories();
  findDirDocumentation(root.get());
  g_s.end();

  g_s.begin("Building namespace list...\n");
  // 命名空间的处理
  buildNamespaceList(root.get());
  findUsingDirectives(root.get());
  g_s.end();

  g_s.begin("Building file list...\n");
  // 文件的处理
  buildFileList(root.get());
  g_s.end();

  g_s.begin("Building class list...\n");
  // 类的处理
  buildClassList(root.get());
  g_s.end();

  g_s.begin("Building concept list...\n");
  // C++ concept 的处理
  buildConceptList(root.get());
  g_s.end();

  // build list of using declarations here (global list)
  // C++ using 的处理,例如 using cout=std::cout
  buildListOfUsingDecls(root.get());
  g_s.end();

  g_s.begin("Computing nesting relations for classes...\n");
  // 解决 C++ 类的嵌套关系
  resolveClassNestingRelations();
  g_s.end();
  // 1.8.2-20121111: no longer add nested classes to the group as well
  //distributeClassGroupRelations();

  // calling buildClassList may result in cached relations that
  // become invalid after resolveClassNestingRelations(), that's why
  // we need to clear the cache here
  Doxygen::typeLookupCache->clear();
  // we don't need the list of using declaration anymore
  g_usingDeclarations.clear();

  g_s.begin("Associating documentation with classes...\n");
  // 构建类文档列表
  buildClassDocList(root.get());
  g_s.end();

  g_s.begin("Associating documentation with concepts...\n");
  // 构建concept的文档列表
  buildConceptDocList(root.get());
  distributeConceptGroups();
  g_s.end();

  g_s.begin("Building example list...\n");
  // 构建 examples 列表
  buildExampleList(root.get());
  g_s.end();

  g_s.begin("Searching for enumerations...\n");
  // 查找枚举类型
  findEnums(root.get());
  g_s.end();

  // Since buildVarList calls isVarWithConstructor
  // and this calls getResolvedClass we need to process
  // typedefs first so the relations between classes via typedefs
  // are properly resolved. See bug 536385 for an example.
  g_s.begin("Searching for documented typedefs...\n");
  // 处理 typedef
  buildTypedefList(root.get());
  g_s.end();

  if (Config_getBool(OPTIMIZE_OUTPUT_SLICE))
  {
    g_s.begin("Searching for documented sequences...\n");
    buildSequenceList(root.get());
    g_s.end();

    g_s.begin("Searching for documented dictionaries...\n");
    buildDictionaryList(root.get());
    g_s.end();
  }

  g_s.begin("Searching for members imported via using declarations...\n");
  // this should be after buildTypedefList in order to properly import
  // used typedefs
  findUsingDeclarations(root.get(),TRUE);  // do for python packages first
  findUsingDeclarations(root.get(),FALSE); // then the rest
  g_s.end();

  g_s.begin("Searching for included using directives...\n");
  findIncludedUsingDirectives();
  g_s.end();

  g_s.begin("Searching for documented variables...\n");
  // 处理变量
  buildVarList(root.get());
  g_s.end();

  g_s.begin("Building interface member list...\n");
  buildInterfaceAndServiceList(root.get()); // UNO IDL

  g_s.begin("Building member list...\n"); // using class info only !
  // 处理函数
  buildFunctionList(root.get());
  g_s.end();

  g_s.begin("Searching for friends...\n");
  // 处理 C++ 友元
  findFriends();
  g_s.end();

  g_s.begin("Searching for documented defines...\n");
  findDefineDocumentation(root.get());
  g_s.end();

  g_s.begin("Computing class inheritance relations...\n");
  findClassEntries(root.get());
  findInheritedTemplateInstances();
  g_s.end();

  g_s.begin("Computing class usage relations...\n");
  // 计算类之间的关系
  findUsedTemplateInstances();
  g_s.end();

  if (Config_getBool(INLINE_SIMPLE_STRUCTS))
  {
    g_s.begin("Searching for tag less structs...\n");
    findTagLessClasses();
    g_s.end();
  }

  g_s.begin("Flushing cached template relations that have become invalid...\n");
  flushCachedTemplateRelations();
  g_s.end();

  g_s.begin("Computing class relations...\n");
  computeTemplateClassRelations();
  flushUnresolvedRelations();
  if (Config_getBool(OPTIMIZE_OUTPUT_VHDL))
  {
    VhdlDocGen::computeVhdlComponentRelations();
  }
  computeClassRelations();
  g_classEntries.clear();
  g_s.end();

  g_s.begin("Add enum values to enums...\n");
  addEnumValuesToEnums(root.get());
  findEnumDocumentation(root.get());
  g_s.end();

  g_s.begin("Searching for member function documentation...\n");
  findObjCMethodDefinitions(root.get());
  findMemberDocumentation(root.get()); // may introduce new members !
  findUsingDeclImports(root.get()); // may introduce new members !

  transferRelatedFunctionDocumentation();
  transferFunctionDocumentation();
  g_s.end();

  // moved to after finding and copying documentation,
  // as this introduces new members see bug 722654
  g_s.begin("Creating members for template instances...\n");
  // 处理模板实例的成员
  createTemplateInstanceMembers();
  g_s.end();

  g_s.begin("Building page list...\n");
  // 处理页面
  buildPageList(root.get());
  g_s.end();

  g_s.begin("Search for main page...\n");
  // 文档主页
  findMainPage(root.get());
  findMainPageTagFiles(root.get());
  g_s.end();

  g_s.begin("Computing page relations...\n");
  computePageRelations(root.get());
  checkPageRelations();
  g_s.end();

  g_s.begin("Determining the scope of groups...\n");
  findGroupScope(root.get());
  g_s.end();

  // 成员名字比较函数, 用于排序
  auto memberNameComp = [](const MemberNameLinkedMap::Ptr &n1,const MemberNameLinkedMap::Ptr &n2)
  {
    return qstricmp(n1->memberName().data()+getPrefixIndex(n1->memberName()),
                    n2->memberName().data()+getPrefixIndex(n2->memberName())
                   )<0;
  };

  // 类比较函数, 用于排序
  auto classComp = [](const ClassLinkedMap::Ptr &c1,const ClassLinkedMap::Ptr &c2)
  {
    if (Config_getBool(SORT_BY_SCOPE_NAME))
    {
      return qstricmp(c1->name(), c2->name())<0;
    }
    else
    {
      int i = qstricmp(c1->className(), c2->className());
      return i==0 ? qstricmp(c1->name(), c2->name())<0 : i<0;
    }
  };

  // 命名空间比较函数,用于排序
  auto namespaceComp = [](const NamespaceLinkedMap::Ptr &n1,const NamespaceLinkedMap::Ptr &n2)
  {
    return qstricmp(n1->name(),n2->name())<0;
  };

  // concept 比较函数, 用于排序
  auto conceptComp = [](const ConceptLinkedMap::Ptr &c1,const ConceptLinkedMap::Ptr &c2)
  {
    return qstricmp(c1->name(),c2->name())<0;
  };

  // 排序列表
  g_s.begin("Sorting lists...\n");
  std::sort(Doxygen::memberNameLinkedMap->begin(),
            Doxygen::memberNameLinkedMap->end(),
            memberNameComp);
  std::sort(Doxygen::functionNameLinkedMap->begin(),
            Doxygen::functionNameLinkedMap->end(),
            memberNameComp);
  std::sort(Doxygen::hiddenClassLinkedMap->begin(),
            Doxygen::hiddenClassLinkedMap->end(),
            classComp);
  std::sort(Doxygen::classLinkedMap->begin(),
            Doxygen::classLinkedMap->end(),
            classComp);
  std::sort(Doxygen::conceptLinkedMap->begin(),
            Doxygen::conceptLinkedMap->end(),
            conceptComp);
  std::sort(Doxygen::namespaceLinkedMap->begin(),
            Doxygen::namespaceLinkedMap->end(),
            namespaceComp);
  g_s.end();

  g_s.begin("Determining which enums are documented\n");
  // 寻找被文档标注的枚举值
  findDocumentedEnumValues();
  g_s.end();

  g_s.begin("Computing member relations...\n");
  // 合并类别
  mergeCategories();
  // 计算成员关系
  computeMemberRelations();
  g_s.end();

  g_s.begin("Building full member lists recursively...\n");
  buildCompleteMemberLists();
  g_s.end();

  g_s.begin("Adding members to member groups.\n");
  addMembersToMemberGroup();
  g_s.end();

  if (Config_getBool(DISTRIBUTE_GROUP_DOC))
  {
    g_s.begin("Distributing member group documentation.\n");
    distributeMemberGroupDocumentation();
    g_s.end();
  }

  g_s.begin("Computing member references...\n");
  computeMemberReferences();
  g_s.end();

  if (Config_getBool(INHERIT_DOCS))
  {
    g_s.begin("Inheriting documentation...\n");
    inheritDocumentation();
    g_s.end();
  }

  // compute the shortest possible names of all files
  // without losing the uniqueness of the file names.
  g_s.begin("Generating disk names...\n");
  generateDiskNames();
  g_s.end();

  g_s.begin("Adding source references...\n");
  addSourceReferences();
  g_s.end();

  g_s.begin("Adding xrefitems...\n");
  addListReferences();
  generateXRefPages();
  g_s.end();

  g_s.begin("Sorting member lists...\n");
  sortMemberLists();
  g_s.end();

  g_s.begin("Setting anonymous enum type...\n");
  setAnonymousEnumType();
  g_s.end();

  if (Config_getBool(DIRECTORY_GRAPH))
  {
    g_s.begin("Computing dependencies between directories...\n");
    computeDirDependencies();
    g_s.end();
  }

  g_s.begin("Generating citations page...\n");
  CitationManager::instance().generatePage();
  g_s.end();

  g_s.begin("Counting members...\n");
  countMembers();
  g_s.end();

  g_s.begin("Counting data structures...\n");
  Index::instance().countDataStructures();
  g_s.end();

  g_s.begin("Resolving user defined references...\n");
  resolveUserReferences();
  g_s.end();

  g_s.begin("Finding anchors and sections in the documentation...\n");
  findSectionsInDocumentation();
  g_s.end();

  g_s.begin("Transferring function references...\n");
  transferFunctionReferences();
  g_s.end();

  g_s.begin("Combining using relations...\n");
  combineUsingRelations();
  g_s.end();

  initSearchIndexer();
  g_s.begin("Adding members to index pages...\n");
  addMembersToIndex();
  addToIndices();
  g_s.end();

  g_s.begin("Correcting members for VHDL...\n");
  vhdlCorrectMemberProperties();
  g_s.end();

  g_s.begin("Computing tooltip texts...\n");
  computeTooltipTexts();
  g_s.end();

  if (Config_getBool(SORT_GROUP_NAMES))
  {
    std::sort(Doxygen::groupLinkedMap->begin(),
              Doxygen::groupLinkedMap->end(),
              [](const auto &g1,const auto &g2)
              { return g1->groupTitle() < g2->groupTitle(); });

    for (const auto &gd : *Doxygen::groupLinkedMap)
    {
      gd->sortSubGroups();
    }
  }

}

8. generateOutput(): 生成输出的过程浅析

  • dump 所有的符号表
  • 生成 html
  • 生成 latex
  • 生成 man
  • 生成 docbook
  • 生成 RTF
  • 生成 HTAGS

这里主要关注 html, 其他格式忽略。 html 相关的具体生成, 涉及如下功能/函数调用:

  • 生成图片 generateImages()
  • 生成例子文档 generateExampleDocs()
  • 生成文件源代码 generateFileSources()
  • 生成文件列表 generateFileDocs()
  • 生成page页面 generatePageDocs()
  • 生成group页面 generateGroupDocs()
  • 生成class页面 generateClassDocs()
  • 生成concept页面 generateConceptDocs()
  • 生成命名空间页面 generateNamespaceDocs()
  • 生成目录页面 generateDirDocs()
  • 生成 tag 文件 writeTagFile()
  • 生成 html 搜索页面 writeSearchPage()
  • 生成 .chm 电子书 runHtmlHelpCompiler()
  • 输出统计信息(文档生成耗时)

代码过多, 这一小节没有具体的注释, 但后续值得具体分析和调试。

// src/doxygen.cpp L12350-L12718
void generateOutput()
{
  AUTO_TRACE();
  /**************************************************************************
   *            Initialize output generators                                *
   **************************************************************************/

  /// add extra languages for which we can only produce syntax highlighted code
  addCodeOnlyMappings();

   dump all symbols
  if (g_dumpSymbolMap)
  {
    dumpSymbolMap();
    exit(0);
  }

  bool generateHtml  = Config_getBool(GENERATE_HTML);
  bool generateLatex = Config_getBool(GENERATE_LATEX);
  bool generateMan   = Config_getBool(GENERATE_MAN);
  bool generateRtf   = Config_getBool(GENERATE_RTF);
  bool generateDocbook = Config_getBool(GENERATE_DOCBOOK);


  g_outputList = new OutputList;
  if (generateHtml)
  {
    g_outputList->add<HtmlGenerator>();
    HtmlGenerator::init();
    HtmlGenerator::writeTabData();
  }
  if (generateLatex)
  {
    g_outputList->add<LatexGenerator>();
    LatexGenerator::init();
  }
  if (generateDocbook)
  {
    g_outputList->add<DocbookGenerator>();
    DocbookGenerator::init();
  }
  if (generateMan)
  {
    g_outputList->add<ManGenerator>();
    ManGenerator::init();
  }
  if (generateRtf)
  {
    g_outputList->add<RTFGenerator>();
    RTFGenerator::init();
  }
  if (Config_getBool(USE_HTAGS))
  {
    Htags::useHtags = TRUE;
    QCString htmldir = Config_getString(HTML_OUTPUT);
    if (!Htags::execute(htmldir))
       err("USE_HTAGS is YES but htags(1) failed. \n");
    else if (!Htags::loadFilemap(htmldir))
       err("htags(1) ended normally but failed to load the filemap. \n");
  }

  /**************************************************************************
   *                        Generate documentation                          *
   **************************************************************************/

  g_s.begin("Generating style sheet...\n");
  //printf("writing style info\n");
  g_outputList->writeStyleInfo(0); // write first part
  g_s.end();

  bool searchEngine      = Config_getBool(SEARCHENGINE);
  bool serverBasedSearch = Config_getBool(SERVER_BASED_SEARCH);

  g_s.begin("Generating search indices...\n");
  if (searchEngine && !serverBasedSearch && (generateHtml || g_useOutputTemplate))
  {
    createJavaScriptSearchIndex();
  }

  // generate search indices (need to do this before writing other HTML
  // pages as these contain a drop down menu with options depending on
  // what categories we find in this function.
  if (generateHtml && searchEngine)
  {
    QCString searchDirName = Config_getString(HTML_OUTPUT)+"/search";
    Dir searchDir(searchDirName.str());
    if (!searchDir.exists() && !searchDir.mkdir(searchDirName.str()))
    {
      term("Could not create search results directory '%s' $PWD='%s'\n",
          qPrint(searchDirName),Dir::currentDirPath().c_str());
    }
    HtmlGenerator::writeSearchData(searchDirName);
    if (!serverBasedSearch) // client side search index
    {
      writeJavaScriptSearchIndex();
    }
  }
  g_s.end();

  // copy static stuff
  if (generateHtml)
  {
    FTVHelp::generateTreeViewImages();
    copyStyleSheet();
    copyLogo(Config_getString(HTML_OUTPUT));
    copyExtraFiles(Config_getList(HTML_EXTRA_FILES),"HTML_EXTRA_FILES",Config_getString(HTML_OUTPUT));
  }
  if (generateLatex)
  {
    copyLatexStyleSheet();
    copyLogo(Config_getString(LATEX_OUTPUT));
    copyExtraFiles(Config_getList(LATEX_EXTRA_FILES),"LATEX_EXTRA_FILES",Config_getString(LATEX_OUTPUT));
  }
  if (generateDocbook)
  {
    copyLogo(Config_getString(DOCBOOK_OUTPUT));
  }
  if (generateRtf)
  {
    copyLogo(Config_getString(RTF_OUTPUT));
  }

  FormulaManager &fm = FormulaManager::instance();
  if (fm.hasFormulas() && generateHtml
      && !Config_getBool(USE_MATHJAX))
  {
    g_s.begin("Generating images for formulas in HTML...\n");
    fm.generateImages(Config_getString(HTML_OUTPUT), Config_getEnum(HTML_FORMULA_FORMAT)==HTML_FORMULA_FORMAT_t::svg ?
        FormulaManager::Format::Vector : FormulaManager::Format::Bitmap, FormulaManager::HighDPI::On);
    g_s.end();
  }
  if (fm.hasFormulas() && generateRtf)
  {
    g_s.begin("Generating images for formulas in RTF...\n");
    fm.generateImages(Config_getString(RTF_OUTPUT),FormulaManager::Format::Bitmap);
    g_s.end();
  }

  if (fm.hasFormulas() && generateDocbook)
  {
    g_s.begin("Generating images for formulas in Docbook...\n");
    fm.generateImages(Config_getString(DOCBOOK_OUTPUT),FormulaManager::Format::Bitmap);
    g_s.end();
  }

  g_s.begin("Generating example documentation...\n");
  generateExampleDocs();
  g_s.end();

  warn_flush();

  g_s.begin("Generating file sources...\n");
  generateFileSources();
  g_s.end();

  g_s.begin("Generating file documentation...\n");
  generateFileDocs();
  g_s.end();

  g_s.begin("Generating page documentation...\n");
  generatePageDocs();
  g_s.end();

  g_s.begin("Generating group documentation...\n");
  generateGroupDocs();
  g_s.end();

  g_s.begin("Generating class documentation...\n");
  generateClassDocs();
  g_s.end();

  g_s.begin("Generating concept documentation...\n");
  generateConceptDocs();
  g_s.end();

  g_s.begin("Generating namespace documentation...\n");
  generateNamespaceDocs();
  g_s.end();

  if (Config_getBool(GENERATE_LEGEND))
  {
    g_s.begin("Generating graph info page...\n");
    writeGraphInfo(*g_outputList);
    g_s.end();
  }

  g_s.begin("Generating directory documentation...\n");
  generateDirDocs(*g_outputList);
  g_s.end();

  if (g_outputList->size()>0)
  {
    writeIndexHierarchy(*g_outputList);
  }

  g_s.begin("finalizing index lists...\n");
  Doxygen::indexList->finalize();
  g_s.end();

  g_s.begin("writing tag file...\n");
  writeTagFile();
  g_s.end();

  if (Config_getBool(GENERATE_XML))
  {
    g_s.begin("Generating XML output...\n");
    Doxygen::generatingXmlOutput=TRUE;
    generateXML();
    Doxygen::generatingXmlOutput=FALSE;
    g_s.end();
  }
#if USE_SQLITE3
  if (Config_getBool(GENERATE_SQLITE3))
  {
    g_s.begin("Generating SQLITE3 output...\n");
    generateSqlite3();
    g_s.end();
  }
#endif

  if (Config_getBool(GENERATE_AUTOGEN_DEF))
  {
    g_s.begin("Generating AutoGen DEF output...\n");
    generateDEF();
    g_s.end();
  }
  if (Config_getBool(GENERATE_PERLMOD))
  {
    g_s.begin("Generating Perl module output...\n");
    generatePerlMod();
    g_s.end();
  }
  if (generateHtml && searchEngine && serverBasedSearch)
  {
    g_s.begin("Generating search index\n");
    if (Doxygen::searchIndex->kind()==SearchIndexIntf::Internal) // write own search index
    {
      HtmlGenerator::writeSearchPage();
      Doxygen::searchIndex->write(Config_getString(HTML_OUTPUT)+"/search/search.idx");
    }
    else // write data for external search index
    {
      HtmlGenerator::writeExternalSearchPage();
      QCString searchDataFile = Config_getString(SEARCHDATA_FILE);
      if (searchDataFile.isEmpty())
      {
        searchDataFile="searchdata.xml";
      }
      if (!Portable::isAbsolutePath(searchDataFile.data()))
      {
        searchDataFile.prepend(Config_getString(OUTPUT_DIRECTORY)+"/");
      }
      Doxygen::searchIndex->write(searchDataFile);
    }
    g_s.end();
  }

  if (g_useOutputTemplate)
  {
    g_s.begin("Generating output via template engine...\n");
    generateOutputViaTemplate();
    g_s.end();
  }

  warn_flush();

  if (generateRtf)
  {
    g_s.begin("Combining RTF output...\n");
    if (!RTFGenerator::preProcessFileInplace(Config_getString(RTF_OUTPUT),"refman.rtf"))
    {
      err("An error occurred during post-processing the RTF files!\n");
    }
    g_s.end();
  }

  warn_flush();

  g_s.begin("Running plantuml with JAVA...\n");
  PlantumlManager::instance().run();
  g_s.end();

  warn_flush();

  if (Config_getBool(HAVE_DOT))
  {
    g_s.begin("Running dot...\n");
    DotManager::instance()->run();
    g_s.end();
  }

  if (generateHtml &&
      Config_getBool(GENERATE_HTMLHELP) &&
      !Config_getString(HHC_LOCATION).isEmpty())
  {
    g_s.begin("Running html help compiler...\n");
    runHtmlHelpCompiler();
    g_s.end();
  }

  warn_flush();

  if ( generateHtml &&
       Config_getBool(GENERATE_QHP) &&
      !Config_getString(QHG_LOCATION).isEmpty())
  {
    g_s.begin("Running qhelpgenerator...\n");
    runQHelpGenerator();
    g_s.end();
  }

  g_outputList->cleanup();

  msg("type lookup cache used %zu/%zu hits=%" PRIu64 " misses=%" PRIu64 "\n",
      Doxygen::typeLookupCache->size(),
      Doxygen::typeLookupCache->capacity(),
      Doxygen::typeLookupCache->hits(),
      Doxygen::typeLookupCache->misses());
  msg("symbol lookup cache used %zu/%zu hits=%" PRIu64 " misses=%" PRIu64 "\n",
      Doxygen::symbolLookupCache->size(),
      Doxygen::symbolLookupCache->capacity(),
      Doxygen::symbolLookupCache->hits(),
      Doxygen::symbolLookupCache->misses());
  int typeCacheParam   = computeIdealCacheParam(static_cast<size_t>(Doxygen::typeLookupCache->misses()*2/3)); // part of the cache is flushed, hence the 2/3 correction factor
  int symbolCacheParam = computeIdealCacheParam(static_cast<size_t>(Doxygen::symbolLookupCache->misses()));
  int cacheParam = std::max(typeCacheParam,symbolCacheParam);
  if (cacheParam>Config_getInt(LOOKUP_CACHE_SIZE))
  {
    msg("Note: based on cache misses the ideal setting for LOOKUP_CACHE_SIZE is %d at the cost of higher memory usage.\n",cacheParam);
  }

  if (Debug::isFlagSet(Debug::Time))
  {

    std::size_t numThreads = static_cast<std::size_t>(Config_getInt(NUM_PROC_THREADS));
    if (numThreads<1) numThreads=1;
    msg("Total elapsed time: %.6f seconds\n(of which an average of %.6f seconds per thread waiting for external tools to finish)\n",
         (static_cast<double>(Debug::elapsedTime())),
         Portable::getSysElapsedTime()/static_cast<double>(numThreads)
        );
    g_s.print();

    Debug::clearFlag(Debug::Time);
    msg("finished...\n");
    Debug::setFlag(Debug::Time);
  }
  else
  {
    msg("finished...\n");
  }


  /**************************************************************************
   *                        Start cleaning up                               *
   **************************************************************************/

  cleanUpDoxygen();

  finalizeSearchIndexer();
  Dir thisDir;
  thisDir.remove(Doxygen::filterDBFileName.str());
  finishWarnExit();
  exitTracing();
  Config::deinit();
  delete Doxygen::clangUsrMap;
  g_successfulRun=TRUE;

  //dumpDocNodeSizes();
}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值