[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"consumer-news-detail-1025":3,"consumer-news-interaction-1025":41,"consumer-news-related-1025":44},{"detail":4,"item":36},{"card":5,"schemaVersion":23,"fields":24,"content":30},{"id":6,"kind":7,"targetType":8,"targetId":9,"subtype":7,"typeLabel":10,"title":11,"subtitle":12,"summary":13,"coverUrl":14,"badgeText":15,"href":16,"sourceName":12,"meta":17,"metrics":20,"tags":21,"resolved":22},"NEWS_ARTICLE:1025","news","NEWS_ARTICLE",1025,"资讯","Ninja 使用笔记","博客园","title: Ninja 使用笔记 author: 凌杰 date: 2026-08-10 tags: 自动化构建 categories: 软件使用经验 [!NOTE] 笔记说明 这篇笔记是《[[Makefile 使用笔记]]》的姊妹篇，将用于记录本人在使用 Ninja 这款项目构建工具过程中所记录","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260906155027127-1498408465.png","","\u002Fnews\u002F1025",[18,19],"2026","综合技术",{},[19],true,"consumer-content-detail-v1",{"sourceName":12,"authorName":25,"categoryName":19,"summary":13,"description":13,"publishTime":26,"updateTime":27,"sourceUrl":28,"language":29},"凌杰","2026-09-06T15:50","2026-09-11T15:22:03","https:\u002F\u002Fwww.cnblogs.com\u002Fowlman\u002Fp\u002F22863314","中文",{"format":31,"policy":32,"normalized":22,"html":33,"text":34,"wordCount":35,"hasBody":22},"HTML","NEWS_CONTENT_V1","\u003Cblockquote>\n \u003Cp>[!NOTE] 笔记说明\u003C\u002Fp>\n \u003Cp>这篇笔记是《[[Makefile 使用笔记]]》的姊妹篇，将用于记录本人在使用 Ninja 这款项目构建工具过程中所记录的心得体会，它侧重于 Ninja 在现代 C\u002FC++ 工程中的实际使用方式。具体内容包括以 CMake 为元构建系统时所生成的\u003Ccode>build.ninja\u003C\u002Fcode>文件的逐段解析思路，以及日常排错时常用的\u003Ccode>ninja\u003C\u002Fcode>命令与参数，便于在阅读和维护 PyTorch 等采用 CMake + Ninja 范式的大型开源项目时能够快速上手。同样的，本笔记也将会被存储在我个人的\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fowlman\u002FCS_Studynotes\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">计算机专业笔记库\u003C\u002Fa> 中，并予以长期维护。\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Ch2>Ninja 简介\u003C\u002Fh2>\n\u003Cp>Ninja 是时下较为流行的一款项目构建工具，主要用于通过调用代码生成器、编译器、链接器等各种工具来完成软件项目的编译工作。如今的很多大型项目（例如 PyTorch）都是采用基于 Ninja 来进行系统构建的，为了阅读并维护这些项目，我们很有必要学习一下这款工具的基本使用方法。\u003C\u002Fp>\n\u003Cp>与我在《[[Makefile 使用笔记]]》中所介绍的那种传统的自动化构建工具不同，ninja 在设计之初就没打算让人类去手动编写项目的构建规则，这些规则基本上是要由专用程序来负责生成的，这可以帮助我们最大限度地避免人类语言不够精确的问题，例如在\u003Ccode>makefile\u003C\u002Fcode>中，我们经常会用\u003Ccode>src\u002F*.c\u003C\u002Fcode>这样的字符串来表示源码文件（其实这包括了文件的路径），这通常需要构建工具去执行遍历目录、匹配文件名才能获得正确的输入。这些操作不仅会拖慢项目的构建速度，且通常还会引入各种不确定的因素。Ninja 选择将这些操作都交给元构建系统（meta-build system，例如 cmake）来处理，自身只负责处理真正需要编译的命令。\u003C\u002Fp>\n\u003Cp>因此在本质上，我们可以认为基于 Ninja 的构建规则就只需要一条一条地列出了具体的命令，然后执行它们即可。在执行过程中，Ninja 会自行去分析这一系列命令之间的依赖关系，并根据依赖关系的不同分以下两种方式来进行处理。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>并行编译\u003C\u002Fstrong>：对彼此没有依赖关系的编译命令采用并行化处理。Ninja 默认使用的并行数为 CPU 数量，除非我们想限制 Ninja 使用的 CPU 数量，一般不用手动设置并行数。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>增量编译\u003C\u002Fstrong>：对彼此有依赖关系的编译命令，分析目标文件的时间戳，如果发现某个文件的时间戳发生了改变，则依赖于该文件的命令以及其他依赖于这个命令的命令都会被重新执行，以此达到增量编译的效果。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>安装方法\u003C\u002Fh2>\n\u003Cp>Ninja 是一个体量很小的 CLI 工具，根据它所要作用的项目环境，我们通常有以下三种安装方式：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cstrong>系统级环境\u003C\u002Fstrong>：可以使用我们所在系统的软件包管理器命令来进行安装，例如 Ubuntu 上的\u003Ccode>apt install ninja-build\u003C\u002Fcode>、Windows 上的\u003Ccode>scoop install ninja\u003C\u002Fcode>、MacOS 上的\u003Ccode>brew install ninja\u003C\u002Fcode>，安装好之后，Ninja 就可以像\u003Ccode>ls\u003C\u002Fcode>、\u003Ccode>cat\u003C\u002Fcode>等系统命令一样被使用了。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>单一项目环境\u003C\u002Fstrong>：可以使用 pip 或 conda 这样的项目依赖管理器命令来进行安装，例如\u003Ccode>conda install ninja\u003C\u002Fcode>或\u003Ccode>pip install ninja\u003C\u002Fcode>。\u003C\u002Fli>\n \u003Cli>\u003Cstrong>自定义环境\u003C\u002Fstrong>：在 GitHub 找到 ninja 项目（如图 1 所示）下载它的安装包并解压即可。当然，如果有特殊需求，也可以选择下载它的源码，然后进行本地编译。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Cimg alt=\"Ninja 项目在 github 上的主页\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260906155027127-1498408465.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Cp>\u003Cstrong>图 1\u003C\u002Fstrong> Ninja 项目在 GitHub 上的主页\u003C\u002Fp>\n\u003Cp>需要特别说明的是：Ninja 的官方项目事实上只会通过 GitHub 来发布他们的新版本，其它获取该工具的渠道都是相关的开发社区自己负责维护的。例如，\u003Ccode>pip install ninja\u003C\u002Fcode>命令安装的是 scikit-build 社区维护的 \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fscikit-build\u002Fninja-python-distributions\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">ninja-python-distributions\u003C\u002Fa>，他们把 Ninja 打包成了一个 pip 包。很多人不知道他们下载的不是官方的版本。同样的，\u003Ccode>conda install ninja\u003C\u002Fcode>命令所安装的也是一个类似的东西。总之，如果读者比较在意 Ninja 的原生功能以及安全问题，那就应该尽可能去 GitHub release 页面下载它的安装包。当然，在大多数情况下，用各种开发社区维护的安装方式已经够满足需求了。\u003C\u002Fp>\n\u003Ch2>理解\u003Ccode>build.ninja\u003C\u002Fcode>\u003C\u002Fh2>\n\u003Cp>基于 Ninja 的项目构建配置文件一般会被命名为\u003Ccode>build.ninja\u003C\u002Fcode>。虽然该文件通常并不需要我们亲自去编写，但基于项目维护方面的考虑，程序员们还是至少要能看得懂它才行。下面，让我们继续基于《[[Makefile 使用笔记]]》中所使用的那个\u003Ccode>calc\u003C\u002Fcode>示例程序来介绍一下\u003Ccode>build.ninja\u003C\u002Fcode>文件中会出现的常见内容。想必读者还记得，这个示例程序的项目结构如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>example\n├── calc                     # 源码：main.c \u002F getch.c \u002F getop.c \u002F stack.c \u002F calc.h\n├── out                      # 构建产物目标目录（makefile 与 cmake 的构建都会把产物放这里）\n├── test                     # 测试：input.txt \u002F run_calc.py\n├── makefile                 # 手写 Makefile 版本（详见《Makefile 使用笔记》）\n└── .gitignore               # 排除 cmake 产生的中间产物\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>之前，我们是使用 make 这种传统的项目构建工具来处理这个 C 项目的。如果现在想改用 Ninja 来构建这个项目，首先要做的是用 CMake 来生成一个\u003Ccode>build.ninja\u003C\u002Fcode>文件。目前最为常见的操作步骤如下。\u003C\u002Fp>\n\u003Col>\n \u003Cli>\u003Cp>确保项目所在的开发环境中已经安装了 gcc\u002Fclang 编译器，以及 CMake、Ninja 构建工具。\u003C\u002Fp>\n  \u003Cblockquote>\n   \u003Cp>关联笔记：[[Clang 使用笔记]] [[CMake 使用笔记]]\u003C\u002Fp>\n  \u003C\u002Fblockquote>\u003C\u002Fli>\n \u003Cli>\u003Cp>在项目的根目录下创建一个名为\u003Ccode>CMakeLists.txt\u003C\u002Fcode>的文件，并写入以下内容：\u003C\u002Fp>\u003Cpre>\u003Ccode> cmake_minimum_required(VERSION 3.10)\n project(calc C)\n\n # ---- 编译选项 ----\n set(CMAKE_C_STANDARD 99)\n set(CMAKE_C_STANDARD_REQUIRED ON)\n set(CMAKE_C_EXTENSIONS OFF)\n\n if(NOT CMAKE_BUILD_TYPE)\n     set(CMAKE_BUILD_TYPE Release CACHE STRING \"Build type\" FORCE)\n endif()\n\n if(CMAKE_C_COMPILER_ID MATCHES \"GNU|Clang\")\n     add_compile_options(-Wall -Wextra)\n endif()\n\n # ---- 目标 ----\n add_executable(calc\n     calc\u002Fmain.c\n     calc\u002Fgetch.c\n     calc\u002Fgetop.c\n     calc\u002Fstack.c\n )\n\n target_include_directories(calc PRIVATE calc)\n\n # 把构建产物（可执行文件）输出到 out\u002F\n # cmake 的配置（CMakeFiles\u002F、CMakeCache.txt、build.ninja）留在源码目录\n set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}\u002Fout)\n set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}\u002Fout)\n set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}\u002Fout)\n\n # ---- 测试 ----\n # 集成 test\u002Frun_calc.py 作为 ctest 用例\n find_package(Python3 COMPONENTS Interpreter QUIET)\n\n if(Python3_Interpreter_FOUND)\n     enable_testing()\n     add_test(\n         NAME calc_pytest\n         COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}\u002Ftest\u002Frun_calc.py\n     )\n else()\n     message(STATUS \"Python3 not found; skipping calc_pytest test target\")\n endif()\n\n # ---- 安装 ----\n include(GNUInstallDirs)\n install(TARGETS calc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})\n\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fli>\n \u003Cli>\u003Cp>在项目根目录下打开命令行终端程序（例如 Powershell、Bash），并执行\u003Ccode>cmake --preset default\u003C\u002Fcode>命令，其执行过程如图 2 所示。\u003C\u002Fp>\u003Cp>\u003Cimg alt=\"cmake 命令执行过程\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260906155027664-244486675.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\u003Cp>\u003Cstrong>图 2\u003C\u002Fstrong> cmake 命令执行过程\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>如果上述过程一切顺利，就会在\u003Ccode>example\u002Fout\u002F\u003C\u002Fcode>目录下看到一个名为\u003Ccode>build.ninja\u003C\u002Fcode>的文件（由 \u003Ccode>CMakePresets.json\u003C\u002Fcode> 里 \u003Ccode>binaryDir: ${sourceDir}\u002Fout\u003C\u002Fcode> 这一项决定）。该文件的内容虽然有 250+ 行，但结构是高度模板化的，下面按从上到下的\"段落\"逐段拆解（下面所有片段都直接取自这篇笔记的示例项目）：\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Cp>\u003Cstrong>文件头声明\u003C\u002Fstrong>。最开头是\u003Ccode>CMAKE generated file: DO NOT EDIT!\u003C\u002Fcode>与版本信息，紧跟一段由注释划分的小节：\u003C\u002Fp>\u003Cpre>\u003Ccode>ninja_required_version = 1.5\nCONFIGURATION = Release\ncmake_ninja_workdir = D$:\u002FWorking\u002Fwriting\u002FCS_StudyNotes\u002F04_软件使用经验\u002FBuilder\u002Fexample\u002Fout\u002F\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>在这里，\u003Ccode>ninja_required_version\u003C\u002Fcode>的作用是让 Ninja 的低版本在不兼容当前配置文件时提早报错；\u003Ccode>CONFIGURATION\u003C\u002Fcode>用于配置项目当前使用的构建类型（\u003Ccode>Debug\u003C\u002Fcode> \u002F \u003Ccode>Release\u003C\u002Fcode>等），后面 rule 的\u003Ccode>FLAGS\u003C\u002Fcode>会根据它切换；\u003Ccode>cmake_ninja_workdir\u003C\u002Fcode>是当前项目根目录所在的绝对路径。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>\u003Ccode>include CMakeFiles\u002Frules.ninja\u003C\u002Fcode>\u003C\u002Fstrong>。真正的\u003Ccode>rule\u003C\u002Fcode>定义都被抽出到\u003Ccode>CMakeFiles\u002Frules.ninja\u003C\u002Fcode>里，主文件通过\u003Ccode>include\u003C\u002Fcode>引入。这种\u003Cstrong>主文件 + 规则子文件\u003C\u002Fstrong>的拆分是 CMake 的惯例：主文件只放\u003Ccode>build\u003C\u002Fcode>条目，规则（编译、链接、自定义命令等）统一放在\u003Ccode>rules.ninja\u003C\u002Fcode>，方便生成器增量更新。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>order-only phony\u003C\u002Fstrong>。每个可执行目标都先声明一个 order-only phony：\u003C\u002Fp>\u003Cpre>\u003Ccode>build cmake_object_order_depends_target_calc: phony || .\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>它的作用是：在编译任何一个\u003Ccode>.o\u003C\u002Fcode>之前，确保工作目录存在；\u003Ccode>||\u003C\u002Fcode>后面是 order-only dependencies（只保证顺序，不参与 mtime 比较）。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>\u003Ccode>.c\u003C\u002Fcode> → \u003Ccode>.o\u003C\u002Fcode> 编译条目\u003C\u002Fstrong>。每个源文件一条 build：\u003C\u002Fp>\u003Cpre>\u003Ccode>build CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fmain.c.obj: C_COMPILER__calc_unscanned_Release\n    D$:\u002FWorking\u002Fwriting\u002FCS_StudyNotes\u002F04_软件使用经验\u002FBuilder\u002Fexample\u002Fcalc\u002Fmain.c\n    || cmake_object_order_depends_target_calc\nCONFIG = Release\nDEP_FILE = CMakeFiles\\calc.dir\\calc\\main.c.obj.d\nFLAGS = -O3 -DNDEBUG -std=c99 -Wall -Wextra\nINCLUDES = -ID:\u002FWorking\u002Fwriting\u002FCS_StudyNotes\u002F04_软件使用经验\u002FBuilder\u002Fexample\u002Fcalc\nOBJECT_DIR = CMakeFiles\u002Fcalc.dir\nOBJECT_FILE_DIR = CMakeFiles\u002Fcalc.dir\u002Fcalc\n...\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>在这里，\u003Ccode>C_COMPILER__calc_unscanned_Release\u003C\u002Fcode>是个 rule 名（定义在 \u003Ccode>rules.ninja\u003C\u002Fcode> 里），冒号后面的\u003Ccode>calc\u002Fmain.c\u003C\u002Fcode>是\u003Ccode>$in\u003C\u002Fcode>。每条 build 自己的局部变量（\u003Ccode>FLAGS\u003C\u002Fcode> \u002F \u003Ccode>INCLUDES\u003C\u002Fcode> \u002F \u003Ccode>OBJECT_DIR\u003C\u002Fcode>等）覆盖同名全局变量，传入对应 rule 的 command。\u003C\u002Fp>\u003Cp>另外，请注意\u003Ccode>DEP_FILE = CMakeFiles\\calc.dir\\calc\\main.c.obj.d\u003C\u002Fcode>这一行。它的作用是让编译器在编译时会把\"该 build 实际 include 了哪些文件\"写到这份\u003Ccode>.d\u003C\u002Fcode>里。这样一来，\u003Ccode>ninja\u003C\u002Fcode>命令下次再执行构建任务时，就会先读\u003Ccode>.d\u003C\u002Fcode>，并把里面的所有路径加入实际依赖列表，再与\u003Ccode>.o\u003C\u002Fcode>的 mtime 比对，任何一个比\u003Ccode>.o\u003C\u002Fcode>新就触发重编。这就是改了\u003Ccode>calc.h\u003C\u002Fcode>也会重编\u003Ccode>main.c\u003C\u002Fcode>的机制基础。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>链接条目\u003C\u002Fstrong>。将所有\u003Ccode>.o\u003C\u002Fcode>汇总成一个名为\u003Ccode>calc\u003C\u002Fcode>的程序（具体到 Windows 环境，就是\u003Ccode>calc.exe\u003C\u002Fcode>），并链接所需库。\u003C\u002Fp>\u003Cpre>\u003Ccode>build calc.exe: C_EXECUTABLE_LINKER__calc_Release\n    CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fmain.c.obj\n    CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fgetch.c.obj\n    CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fgetop.c.obj\n    CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fstack.c.obj\nFLAGS = -O3 -DNDEBUG\nLINK_LIBRARIES = -lkernel32 -luser32 -lgdi32 -lwinspool -lshell32 -lole32 -loleaut32 -luuid -lcomdlg32 -ladvapi32\n\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>utility 命令\u003C\u002Fstrong>。\u003Ccode>test\u003C\u002Fcode> \u002F \u003Ccode>edit_cache\u003C\u002Fcode> \u002F \u003Ccode>rebuild_cache\u003C\u002Fcode> \u002F \u003Ccode>install\u003C\u002Fcode> \u002F \u003Ccode>install\u002Flocal\u003C\u002Fcode> \u002F \u003Ccode>install\u002Fstrip\u003C\u002Fcode> 等子命令也是 build 条目，rule 是 \u003Ccode>CUSTOM_COMMAND\u003C\u002Fcode>，由 \u003Ccode>cmake -P\u003C\u002Fcode> 脚本驱动。例如：\u003C\u002Fp>\u003Cpre>\u003Ccode>build CMakeFiles\u002Ftest.util: CUSTOM_COMMAND\nCOMMAND = C:\\Windows\\system32\\cmd.exe \u002FC \"cd \u002FD ... &amp;&amp; ctest.exe \"\nDESC = Running tests...\npool = console\nrestat = 1\nbuild test: phony CMakeFiles\u002Ftest.util\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>在这里，\u003Ccode>pool = console\u003C\u002Fcode>强制让这些命令串行执行（避免与并行编译抢同一行 stdout），\u003Ccode>restat = 1\u003C\u002Fcode>告诉 ninja 在 command 跑完后重新 stat 一次输出文件（很多 custom command 的输出 mtime 不可靠，需要重 stat）。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>\u003Ccode>RERUN_CMAKE\u003C\u002Fcode> 钩子\u003C\u002Fstrong>。文件末尾有一段很长的 build 条目，触发条件是\u003Ccode>CMakeLists.txt\u003C\u002Fcode>或 CMake 内置 module 发生变化：\u003C\u002Fp>\u003Cpre>\u003Ccode>build build.ninja ... : RERUN_CMAKE | ...\npool = console\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>它的作用是：如果\u003Ccode>CMakeLists.txt\u003C\u002Fcode>被修改了，就先自动重跑 cmake 重新生成\u003Ccode>build.ninja\u003C\u002Fcode>文件本身，再继续构建。这等于把 CMake 自身的再生也嵌进了 ninja 的 DAG 里。\u003C\u002Fp>\u003C\u002Fli>\n \u003Cli>\u003Cp>\u003Cstrong>内建目标 + default\u003C\u002Fstrong>。最后三行：\u003C\u002Fp>\u003Cpre>\u003Ccode>build clean: CLEAN\nbuild help: HELP\ndefault all\n\u003C\u002Fcode>\u003C\u002Fpre>\u003Cp>在这里，\u003Ccode>CLEAN\u003C\u002Fcode>和\u003Ccode>HELP\u003C\u002Fcode>是 ninja 内建规则。\u003Ccode>default all\u003C\u002Fcode>声明裸跑\u003Ccode>ninja\u003C\u002Fcode>命令，等价于\u003Ccode>ninja all\u003C\u002Fcode>；而\u003Ccode>all\u003C\u002Fcode> 在上面被定义为\u003Ccode>phony calc.exe\u003C\u002Fcode>，所以等价于构建\u003Ccode>calc.exe\u003C\u002Fcode>。\u003C\u002Fp>\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>在有了这份 \u003Ccode>build.ninja\u003C\u002Fcode>之后，我们就可以根据自己的需要构建项目了，其常用命令如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ninja              # 等价于 ninja all → 构建 calc.exe\nninja calc         # 只构建 calc alias（phony calc.exe）\nninja test         # 通过 ctest 跑 test\u002Frun_calc.py\nninja clean        # 清理全部构建产物\nninja help         # 列出所有可构建目标\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第一次跑会全量编译\u003Ccode>calc\u002Fmain.c\u003C\u002Fcode>、\u003Ccode>calc\u002Fgetch.c\u003C\u002Fcode>、\u003Ccode>calc\u002Fgetop.c\u003C\u002Fcode>、\u003Ccode>calc\u002Fstack.c\u003C\u002Fcode>四个源文件并链接成\u003Ccode>calc.exe\u003C\u002Fcode>，如图 3 所示。之后如果只改了\u003Ccode>calc\u002Fmain.c\u003C\u002Fcode>，\u003Ccode>ninja\u003C\u002Fcode>就会只重编\u003Ccode>main.c.obj\u003C\u002Fcode>并重新链接。\u003C\u002Fp>\n\u003Cp>另外，读者应该会注意到，由于我们在\u003Ccode>CMakeLists.txt\u003C\u002Fcode>里把\u003Ccode>CMAKE_RUNTIME_OUTPUT_DIRECTORY\u003C\u002Fcode>显式设成了\u003Ccode>${CMAKE_SOURCE_DIR}\u002Fout\u003C\u002Fcode>，可执行文件最终落在\u003Ccode>example\u002Fout\u002Fcalc.exe\u003C\u002Fcode>（而不是源码根目录）。这条约定把源码管理和构建产物做了物理隔离，方便用\u003Ccode>.gitignore\u003C\u002Fcode>分别处理。\u003C\u002Fp>\n\u003Cp>\u003Cimg alt=\"ninja 编译 calc.exe\" src=\"https:\u002F\u002Fi1.wp.com\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260906155027998-1572382995.png?w=720&amp;quality=65&amp;strip=all\">\u003C\u002Fp>\n\u003Cp>\u003Cstrong>图 3\u003C\u002Fstrong> ninja 构建 calc.exe\u003C\u002Fp>\n\u003Ch2>Ninja 进阶用法\u003C\u002Fh2>\n\u003Cp>如果读者回头再看看我们在上述示例中生成的那份\u003Ccode>build.ninja\u003C\u002Fcode>文件，会注意到其中用到了大量的\u003Ccode>phony\u003C\u002Fcode>规则。这是 Ninja 的内建规则，它的作用与\u003Ccode>makefile\u003C\u002Fcode>中的伪目标是相同的，只用于在输入和输出之间建立依赖关系，本身并不代表任何真实文件。我们可以理解为：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>rule phony\n    command = :\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在这里，冒号\u003Ccode>:\u003C\u002Fcode>是类 UNIX 系统里\u003Ccode>true\u003C\u002Fcode>命令的简写，它用于确保不管参数是什么都正常退出；因此\u003Ccode>phony\u003C\u002Fcode>的 command 本身什么都不做，只是把\u003Ccode>$in\u003C\u002Fcode>列为必须先构建好的依赖、把\u003Ccode>$out\u003C\u002Fcode>注册成一个伪目标。仓库的\u003Ccode>build.ninja\u003C\u002Fcode>里至少出现三处：order-only 的\u003Ccode>cmake_object_order_depends_target_calc\u003C\u002Fcode>、聚合用的\u003Ccode>calc\u003C\u002Fcode>、\u003Ccode>all\u003C\u002Fcode>等。\u003C\u002Fp>\n\u003Cp>不带参数的\u003Ccode>ninja\u003C\u002Fcode>命令会构建文件里\u003Ccode>default\u003C\u002Fcode>声明的目标（即上一节末尾那些\u003Ccode>ninja\u003C\u002Fcode> \u002F \u003Ccode>ninja calc\u003C\u002Fcode> \u002F \u003Ccode>ninja test\u003C\u002Fcode>之类）。构建多个目标时 Ninja 会展示进度条，每行的内容来自对应\u003Ccode>build\u003C\u002Fcode>条目下方的\u003Ccode>DESC = ...\u003C\u002Fcode>字段。这篇笔记的示例里就有\u003Ccode>DESC = Running tests...\u003C\u002Fcode>、\u003Ccode>DESC = Install the project...\u003C\u002Fcode>等，进度条会按构建顺序依次打印。\u003C\u002Fp>\n\u003Cp>Ninja 的高级工具主要在\u003Ccode>-t\u003C\u002Fcode>参数下面，常用的子命令如下。\u003C\u002Fp>\n\u003Cul>\n \u003Cli>\u003Ccode>ninja -t targets all\u003C\u002Fcode>：列出全部构建目标（等价于\u003Ccode>ninja help\u003C\u002Fcode>的内容），适合\u003Ccode>grep\u003C\u002Fcode>过滤；\u003C\u002Fli>\n \u003Cli>\u003Ccode>ninja -t clean\u003C\u002Fcode>：与\u003Ccode>ninja clean\u003C\u002Fcode>（即\u003Ccode>build.ninja\u003C\u002Fcode>里\u003Ccode>build clean: CLEAN\u003C\u002Fcode>这条内建目标）等价，删除全部生成文件；\u003C\u002Fli>\n \u003Cli>\u003Ccode>ninja -t deps\u003C\u002Fcode>：扫描\u003Ccode>.ninja_deps\u003C\u002Fcode>数据库，输出每条 build 的依赖关系，方便定位 \"为什么 A 改了会触发 B 重编\"；\u003C\u002Fli>\n \u003Cli>\u003Ccode>ninja -t browse\u003C\u002Fcode>：起一个本地 HTTP 服务（默认\u003Ccode>http:\u002F\u002Flocalhost:8000\u003C\u002Fcode>）用浏览器可视化整张依赖图；\u003Ccode>ninja -t graph\u003C\u002Fcode>则把同一张图导出成 dot 格式供 Graphviz 渲染；\u003C\u002Fli>\n \u003Cli>\u003Ccode>ninja -t compdb\u003C\u002Fcode>：把每条编译命令以 JSON 数组形式输出，常被 clangd \u002F VS Code 等工具用来反查 \"这个 \u003Ccode>.c\u003C\u002Fcode> 对应的编译参数\"，对排查 IDE 索引异常很关键。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>另外，我们还可以用\u003Ccode>ninja -C \u002Fpath\u002Fto\u002Fdir -f \u002Fpath\u002Fto\u002Ffile\u003C\u002Fcode>命令来切换构建根目录与配置文件。在这里，\u003Ccode>-C\u003C\u002Fcode>用于指定目录后再跑命令（默认 \u003Ccode>.\u003C\u002Fcode>），\u003Ccode>-f\u003C\u002Fcode>用于指定要读的配置文件（默认\u003Ccode>build.ninja\u003C\u002Fcode>）。日常使用\u003Ccode>-C\u003C\u002Fcode>多一些，比如用多份构建目录时切换。\u003C\u002Fp>\n\u003Ch2>总结\u003C\u002Fh2>\n\u003Cp>与《[[Makefile 使用笔记]]》中介绍的那种需要手写项目构建规则的范式相比，使用 Ninja 的核心工作流是：程序员们负责人工维护\u003Ccode>CMakeLists.txt\u003C\u002Fcode>（项目元信息），而 CMake 负责先将它编译成\u003Ccode>build.ninja\u003C\u002Fcode>文件（执行计划），再交由 Ninja 去执行\u003Ccode>build.ninja\u003C\u002Fcode>。这两件事的解耦让项目复杂度的天花板被推到了 CMake 那侧。这也是为什么 PyTorch 这样的大型项目选择使用 CMake + Ninja 的范式来替代手写\u003Ccode>makefile\u003C\u002Fcode>文件的根本原因。\u003C\u002Fp>\n\u003Cp>总而言之，\u003Ccode>build.ninja\u003C\u002Fcode>文件通常不是给人手动维护的：它由 CMake 自动生成、文件头就有\u003Ccode>CMAKE generated file: DO NOT EDIT!\u003C\u002Fcode>。本文逐段拆解它的目的，是让读者在 build 报错时能快速定位问题出在 CMake 那边（\u003Ccode>CMakeLists.txt\u003C\u002Fcode>没写对），还是\u003Ccode>ninja\u003C\u002Fcode>命令这边（命令执行环境有问题）。这样一来，我们在日常开发中只要记住：改\u003Ccode>CMakeLists.txt\u003C\u002Fcode>后跑一次\u003Ccode>cmake --preset default\u003C\u002Fcode>命令来重新生成\u003Ccode>build.ninja\u003C\u002Fcode>，之后再用\u003Ccode>ninja\u003C\u002Fcode>命令做增量即可。工具的简洁本身是设计目标 —— Ninja 的核心语法确实就只有这些值得关心的内容。\u003C\u002Fp>\n\u003Ch2>参考资料\u003C\u002Fh2>\n\u003Cul>\n \u003Cli>\u003Ca href=\"https:\u002F\u002Fninja-build.org\u002F\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">Ninja 官方网站\u003C\u002Fa>\u003C\u002Fli>\n \u003Cli>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fninja-build\u002Fninja\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">Ninja 在 GitHub 上的仓库\u003C\u002Fa>\u003C\u002Fli>\n \u003Cli>\u003Ca href=\"https:\u002F\u002Fzhuanlan.zhihu.com\u002Fp\u002F676733751\" target=\"_blank\" rel=\"noopener noreferrer nofollow\">知乎专栏:《一文读懂 ninja 构建系统》（作者：游凯超）\u003C\u002Fa>，\u003C\u002Fli>\n\u003C\u002Ful>","[!NOTE] 笔记说明 这篇笔记是《[[Makefile 使用笔记]]》的姊妹篇，将用于记录本人在使用 Ninja 这款项目构建工具过程中所记录的心得体会，它侧重于 Ninja 在现代 C\u002FC++ 工程中的实际使用方式。具体内容包括以 CMake 为元构建系统时所生成的build.ninja文件的逐段解析思路，以及日常排错时常用的ninja命令与参数，便于在阅读和维护 PyTorch 等采用 CMake + Ninja 范式的大型开源项目时能够快速上手。同样的，本笔记也将会被存储在我个人的计算机专业笔记库 中，并予以长期维护。 Ninja 简介 Ninja 是时下较为流行的一款项目构建工具，主要用于通过调用代码生成器、编译器、链接器等各种工具来完成软件项目的编译工作。如今的很多大型项目（例如 PyTorch）都是采用基于 Ninja 来进行系统构建的，为了阅读并维护这些项目，我们很有必要学习一下这款工具的基本使用方法。 与我在《[[Makefile 使用笔记]]》中所介绍的那种传统的自动化构建工具不同，ninja 在设计之初就没打算让人类去手动编写项目的构建规则，这些规则基本上是要由专用程序来负责生成的，这可以帮助我们最大限度地避免人类语言不够精确的问题，例如在makefile中，我们经常会用src\u002F*.c这样的字符串来表示源码文件（其实这包括了文件的路径），这通常需要构建工具去执行遍历目录、匹配文件名才能获得正确的输入。这些操作不仅会拖慢项目的构建速度，且通常还会引入各种不确定的因素。Ninja 选择将这些操作都交给元构建系统（meta-build system，例如 cmake）来处理，自身只负责处理真正需要编译的命令。 因此在本质上，我们可以认为基于 Ninja 的构建规则就只需要一条一条地列出了具体的命令，然后执行它们即可。在执行过程中，Ninja 会自行去分析这一系列命令之间的依赖关系，并根据依赖关系的不同分以下两种方式来进行处理。 并行编译：对彼此没有依赖关系的编译命令采用并行化处理。Ninja 默认使用的并行数为 CPU 数量，除非我们想限制 Ninja 使用的 CPU 数量，一般不用手动设置并行数。 增量编译：对彼此有依赖关系的编译命令，分析目标文件的时间戳，如果发现某个文件的时间戳发生了改变，则依赖于该文件的命令以及其他依赖于这个命令的命令都会被重新执行，以此达到增量编译的效果。 安装方法 Ninja 是一个体量很小的 CLI 工具，根据它所要作用的项目环境，我们通常有以下三种安装方式： 系统级环境：可以使用我们所在系统的软件包管理器命令来进行安装，例如 Ubuntu 上的apt install ninja-build、Windows 上的scoop install ninja、MacOS 上的brew install ninja，安装好之后，Ninja 就可以像ls、cat等系统命令一样被使用了。 单一项目环境：可以使用 pip 或 conda 这样的项目依赖管理器命令来进行安装，例如conda install ninja或pip install ninja。 自定义环境：在 GitHub 找到 ninja 项目（如图 1 所示）下载它的安装包并解压即可。当然，如果有特殊需求，也可以选择下载它的源码，然后进行本地编译。 图 1 Ninja 项目在 GitHub 上的主页 需要特别说明的是：Ninja 的官方项目事实上只会通过 GitHub 来发布他们的新版本，其它获取该工具的渠道都是相关的开发社区自己负责维护的。例如，pip install ninja命令安装的是 scikit-build 社区维护的 ninja-python-distributions，他们把 Ninja 打包成了一个 pip 包。很多人不知道他们下载的不是官方的版本。同样的，conda install ninja命令所安装的也是一个类似的东西。总之，如果读者比较在意 Ninja 的原生功能以及安全问题，那就应该尽可能去 GitHub release 页面下载它的安装包。当然，在大多数情况下，用各种开发社区维护的安装方式已经够满足需求了。 理解build.ninja 基于 Ninja 的项目构建配置文件一般会被命名为build.ninja。虽然该文件通常并不需要我们亲自去编写，但基于项目维护方面的考虑，程序员们还是至少要能看得懂它才行。下面，让我们继续基于《[[Makefile 使用笔记]]》中所使用的那个calc示例程序来介绍一下build.ninja文件中会出现的常见内容。想必读者还记得，这个示例程序的项目结构如下： example ├── calc # 源码：main.c \u002F getch.c \u002F getop.c \u002F stack.c \u002F calc.h ├── out # 构建产物目标目录（makefile 与 cmake 的构建都会把产物放这里） ├── test # 测试：input.txt \u002F run_calc.py ├── makefile # 手写 Makefile 版本（详见《Makefile 使用笔记》） └── .gitignore # 排除 cmake 产生的中间产物 之前，我们是使用 make 这种传统的项目构建工具来处理这个 C 项目的。如果现在想改用 Ninja 来构建这个项目，首先要做的是用 CMake 来生成一个build.ninja文件。目前最为常见的操作步骤如下。 确保项目所在的开发环境中已经安装了 gcc\u002Fclang 编译器，以及 CMake、Ninja 构建工具。 关联笔记：[[Clang 使用笔记]] [[CMake 使用笔记]] 在项目的根目录下创建一个名为CMakeLists.txt的文件，并写入以下内容： cmake_minimum_required(VERSION 3.10) project(calc C) # ---- 编译选项 ---- set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) set(CMAKE_C_EXTENSIONS OFF) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING \"Build type\" FORCE) endif() if(CMAKE_C_COMPILER_ID MATCHES \"GNU|Clang\") add_compile_options(-Wall -Wextra) endif() # ---- 目标 ---- add_executable(calc calc\u002Fmain.c calc\u002Fgetch.c calc\u002Fgetop.c calc\u002Fstack.c ) target_include_directories(calc PRIVATE calc) # 把构建产物（可执行文件）输出到 out\u002F # cmake 的配置（CMakeFiles\u002F、CMakeCache.txt、build.ninja）留在源码目录 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}\u002Fout) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}\u002Fout) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}\u002Fout) # ---- 测试 ---- # 集成 test\u002Frun_calc.py 作为 ctest 用例 find_package(Python3 COMPONENTS Interpreter QUIET) if(Python3_Interpreter_FOUND) enable_testing() add_test( NAME calc_pytest COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}\u002Ftest\u002Frun_calc.py ) else() message(STATUS \"Python3 not found; skipping calc_pytest test target\") endif() # ---- 安装 ---- include(GNUInstallDirs) install(TARGETS calc RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) 在项目根目录下打开命令行终端程序（例如 Powershell、Bash），并执行cmake --preset default命令，其执行过程如图 2 所示。 图 2 cmake 命令执行过程 如果上述过程一切顺利，就会在example\u002Fout\u002F目录下看到一个名为build.ninja的文件（由 CMakePresets.json 里 binaryDir: ${sourceDir}\u002Fout 这一项决定）。该文件的内容虽然有 250+ 行，但结构是高度模板化的，下面按从上到下的\"段落\"逐段拆解（下面所有片段都直接取自这篇笔记的示例项目）： 文件头声明。最开头是CMAKE generated file: DO NOT EDIT!与版本信息，紧跟一段由注释划分的小节： ninja_required_version = 1.5 CONFIGURATION = Release cmake_ninja_workdir = D$:\u002FWorking\u002Fwriting\u002FCS_StudyNotes\u002F04_软件使用经验\u002FBuilder\u002Fexample\u002Fout\u002F 在这里，ninja_required_version的作用是让 Ninja 的低版本在不兼容当前配置文件时提早报错；CONFIGURATION用于配置项目当前使用的构建类型（Debug \u002F Release等），后面 rule 的FLAGS会根据它切换；cmake_ninja_workdir是当前项目根目录所在的绝对路径。 include CMakeFiles\u002Frules.ninja。真正的rule定义都被抽出到CMakeFiles\u002Frules.ninja里，主文件通过include引入。这种主文件 + 规则子文件的拆分是 CMake 的惯例：主文件只放build条目，规则（编译、链接、自定义命令等）统一放在rules.ninja，方便生成器增量更新。 order-only phony。每个可执行目标都先声明一个 order-only phony： build cmake_object_order_depends_target_calc: phony || . 它的作用是：在编译任何一个.o之前，确保工作目录存在；||后面是 order-only dependencies（只保证顺序，不参与 mtime 比较）。 .c → .o 编译条目。每个源文件一条 build： build CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fmain.c.obj: C_COMPILER__calc_unscanned_Release D$:\u002FWorking\u002Fwriting\u002FCS_StudyNotes\u002F04_软件使用经验\u002FBuilder\u002Fexample\u002Fcalc\u002Fmain.c || cmake_object_order_depends_target_calc CONFIG = Release DEP_FILE = CMakeFiles\\calc.dir\\calc\\main.c.obj.d FLAGS = -O3 -DNDEBUG -std=c99 -Wall -Wextra INCLUDES = -ID:\u002FWorking\u002Fwriting\u002FCS_StudyNotes\u002F04_软件使用经验\u002FBuilder\u002Fexample\u002Fcalc OBJECT_DIR = CMakeFiles\u002Fcalc.dir OBJECT_FILE_DIR = CMakeFiles\u002Fcalc.dir\u002Fcalc ... 在这里，C_COMPILER__calc_unscanned_Release是个 rule 名（定义在 rules.ninja 里），冒号后面的calc\u002Fmain.c是$in。每条 build 自己的局部变量（FLAGS \u002F INCLUDES \u002F OBJECT_DIR等）覆盖同名全局变量，传入对应 rule 的 command。 另外，请注意DEP_FILE = CMakeFiles\\calc.dir\\calc\\main.c.obj.d这一行。它的作用是让编译器在编译时会把\"该 build 实际 include 了哪些文件\"写到这份.d里。这样一来，ninja命令下次再执行构建任务时，就会先读.d，并把里面的所有路径加入实际依赖列表，再与.o的 mtime 比对，任何一个比.o新就触发重编。这就是改了calc.h也会重编main.c的机制基础。 链接条目。将所有.o汇总成一个名为calc的程序（具体到 Windows 环境，就是calc.exe），并链接所需库。 build calc.exe: C_EXECUTABLE_LINKER__calc_Release CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fmain.c.obj CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fgetch.c.obj CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fgetop.c.obj CMakeFiles\u002Fcalc.dir\u002Fcalc\u002Fstack.c.obj FLAGS = -O3 -DNDEBUG LINK_LIBRARIES = -lkernel32 -luser32 -lgdi32 -lwinspool -lshell32 -lole32 -loleaut32 -luuid -lcomdlg32 -ladvapi32 utility 命令。test \u002F edit_cache \u002F rebuild_cache \u002F install \u002F install\u002Flocal \u002F install\u002Fstrip 等子命令也是 build 条目，rule 是 CUSTOM_COMMAND，由 cmake -P 脚本驱动。例如： build CMakeFiles\u002Ftest.util: CUSTOM_COMMAND COMMAND = C:\\Windows\\system32\\cmd.exe \u002FC \"cd \u002FD ... && ctest.exe \" DESC = Running tests... pool = console restat = 1 build test: phony CMakeFiles\u002Ftest.util 在这里，pool = console强制让这些命令串行执行（避免与并行编译抢同一行 stdout），restat = 1告诉 ninja 在 command 跑完后重新 stat 一次输出文件（很多 custom command 的输出 mtime 不可靠，需要重 stat）。 RERUN_CMAKE 钩子。文件末尾有一段很长的 build 条目，触发条件是CMakeLists.txt或 CMake 内置 module 发生变化： build build.ninja ... : RERUN_CMAKE | ... pool = console 它的作用是：如果CMakeLists.txt被修改了，就先自动重跑 cmake 重新生成build.ninja文件本身，再继续构建。这等于把 CMake 自身的再生也嵌进了 ninja 的 DAG 里。 内建目标 + default。最后三行： build clean: CLEAN build help: HELP default all 在这里，CLEAN和HELP是 ninja 内建规则。default all声明裸跑ninja命令，等价于ninja all；而all 在上面被定义为phony calc.exe，所以等价于构建calc.exe。 在有了这份 build.ninja之后，我们就可以根据自己的需要构建项目了，其常用命令如下： ninja # 等价于 ninja all → 构建 calc.exe ninja calc # 只构建 calc alias（phony calc.exe） ninja test # 通过 ctest 跑 test\u002Frun_calc.py ninja clean # 清理全部构建产物 ninja help # 列出所有可构建目标 第一次跑会全量编译calc\u002Fmain.c、calc\u002Fgetch.c、calc\u002Fgetop.c、calc\u002Fstack.c四个源文件并链接成calc.exe，如图 3 所示。之后如果只改了calc\u002Fmain.c，ninja就会只重编main.c.obj并重新链接。 另外，读者应该会注意到，由于我们在CMakeLists.txt里把CMAKE_RUNTIME_OUTPUT_DIRECTORY显式设成了${CMAKE_SOURCE_DIR}\u002Fout，可执行文件最终落在example\u002Fout\u002Fcalc.exe（而不是源码根目录）。这条约定把源码管理和构建产物做了物理隔离，方便用.gitignore分别处理。 图 3 ninja 构建 calc.exe Ninja 进阶用法 如果读者回头再看看我们在上述示例中生成的那份build.ninja文件，会注意到其中用到了大量的phony规则。这是 Ninja 的内建规则，它的作用与makefile中的伪目标是相同的，只用于在输入和输出之间建立依赖关系，本身并不代表任何真实文件。我们可以理解为： rule phony command = : 在这里，冒号:是类 UNIX 系统里true命令的简写，它用于确保不管参数是什么都正常退出；因此phony的 command 本身什么都不做，只是把$in列为必须先构建好的依赖、把$out注册成一个伪目标。仓库的build.ninja里至少出现三处：order-only 的cmake_object_order_depends_target_calc、聚合用的calc、all等。 不带参数的ninja命令会构建文件里default声明的目标（即上一节末尾那些ninja \u002F ninja calc \u002F ninja test之类）。构建多个目标时 Ninja 会展示进度条，每行的内容来自对应build条目下方的DESC = ...字段。这篇笔记的示例里就有DESC = Running tests...、DESC = Install the project...等，进度条会按构建顺序依次打印。 Ninja 的高级工具主要在-t参数下面，常用的子命令如下。 ninja -t targets all：列出全部构建目标（等价于ninja help的内容），适合grep过滤； ninja -t clean：与ninja clean（即build.ninja里build clean: CLEAN这条内建目标）等价，删除全部生成文件； ninja -t deps：扫描.ninja_deps数据库，输出每条 build 的依赖关系，方便定位 \"为什么 A 改了会触发 B 重编\"； ninja -t browse：起一个本地 HTTP 服务（默认http:\u002F\u002Flocalhost:8000）用浏览器可视化整张依赖图；ninja -t graph则把同一张图导出成 dot 格式供 Graphviz 渲染； ninja -t compdb：把每条编译命令以 JSON 数组形式输出，常被 clangd \u002F VS Code 等工具用来反查 \"这个 .c 对应的编译参数\"，对排查 IDE 索引异常很关键。 另外，我们还可以用ninja -C \u002Fpath\u002Fto\u002Fdir -f \u002Fpath\u002Fto\u002Ffile命令来切换构建根目录与配置文件。在这里，-C用于指定目录后再跑命令（默认 .），-f用于指定要读的配置文件（默认build.ninja）。日常使用-C多一些，比如用多份构建目录时切换。 总结 与《[[Makefile 使用笔记]]》中介绍的那种需要手写项目构建规则的范式相比，使用 Ninja 的核心工作流是：程序员们负责人工维护CMakeLists.txt（项目元信息），而 CMake 负责先将它编译成build.ninja文件（执行计划），再交由 Ninja 去执行build.ninja。这两件事的解耦让项目复杂度的天花板被推到了 CMake 那侧。这也是为什么 PyTorch 这样的大型项目选择使用 CMake + Ninja 的范式来替代手写makefile文件的根本原因。 总而言之，build.ninja文件通常不是给人手动维护的：它由 CMake 自动生成、文件头就有CMAKE generated file: DO NOT EDIT!。本文逐段拆解它的目的，是让读者在 build 报错时能快速定位问题出在 CMake 那边（CMakeLists.txt没写对），还是ninja命令这边（命令执行环境有问题）。这样一来，我们在日常开发中只要记住：改CMakeLists.txt后跑一次cmake --preset default命令来重新生成build.ninja，之后再用ninja命令做增量即可。工具的简洁本身是设计目标 —— Ninja 的核心语法确实就只有这些值得关心的内容。 参考资料 Ninja 官方网站 Ninja 在 GitHub 上的仓库 知乎专栏:《一文读懂 ninja 构建系统》（作者：游凯超），",8359,{"id":6,"kind":7,"title":11,"summary":13,"image":14,"href":16,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":40},"2026 · 综合技术","#2563eb","16 \u002F 10",[19],{"targetType":8,"targetId":9,"likedByMe":42,"likeCount":43,"commentCount":43,"contentLikeCount":43,"contentCommentCount":43,"sourceLikeCount":43,"sourceCommentCount":43},false,0,[45,52,58,64,71,78,84,91],{"id":46,"kind":7,"title":47,"summary":48,"image":49,"href":50,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":51},"NEWS_ARTICLE:962","基于增强QUIC协议优化弱网下的直播观看体验","如今，直播已成为电商、教育、娱乐等各领域信息传递与线上消费的重要载体，而网络质量则是决定直播体验的核心命脉。然而，当用户一旦置身于地铁、户外等弱网环境中，画面卡顿、画质模糊、音画错位等问题便接踵而至，原本精彩的直播瞬间变得支离破碎，用户观看体验随之直线下降。 基于此，HarmonyOS SDK远场通","https:\u002F\u002Foscimg.oschina.net\u002F\u002FAiCreationDetail\u002Fup-d5a5690fbed726571a684c85eec1789e.png","\u002Fnews\u002F962",[19],{"id":53,"kind":7,"title":54,"summary":55,"image":15,"href":56,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":57},"NEWS_ARTICLE:1011","上周热点回顾（8.31-9.6）","热点随笔： &#183; 被罚了500后，整个人都变老实了 (欢醉) &#183; 2016已经是十年前了 (三范式) &#183; 都是 AI 写代码，为什么 C# 比 Java 快半拍 (张善友) &#183; OpenClaw 2.0 发布：史上最大更新，8 大新功能 + 2 个破坏性变更必看","\u002Fnews\u002F1011",[19],{"id":59,"kind":7,"title":60,"summary":61,"image":15,"href":62,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":63},"NEWS_ARTICLE:1023","数位 DP 进阶","数位 DP 进阶 日期：2026-07-14 关键词：动态规划、数位 DP、记忆化搜索 涉及题目：P2602 [ZJOI2010] 数字计数 | P4124 [CQOI2016] 手机号码 | P3286 [SCOI2014] 方伯伯的商场之旅 数位 DP 梳理一下数位 DP 的核心思想。 数位 D","\u002Fnews\u002F1023",[19],{"id":65,"kind":7,"title":66,"summary":67,"image":68,"href":69,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":70},"NEWS_ARTICLE:838","AI 学习笔记：LLM 的微调实验","title: LLM 的微调实验 author: 凌杰 date: 2026-08-21 tags: LoRA, LLaMA-Factory, Qwen categories: 人工智能 [!NOTE] 笔记说明 这篇笔记对应的是《[[关于 AI 的学习路线图]]》一文中所规划的第三个学习阶段。其中","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F691082\u002F202609\u002F691082-20260902122904258-1285666483.png","\u002Fnews\u002F838",[19],{"id":72,"kind":7,"title":73,"summary":74,"image":75,"href":76,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":77},"NEWS_ARTICLE:843","介绍一下常用的Token鉴权方案","本文梳理 Session‑Cookie、JWT、OAuth2.0、SSO 主流 Token 鉴权方案，对比各方案适用场景。重点讲解生产级 JWT 双 Token 架构，给出 RS256 非对称加密、Redis 黑名单、网关统一鉴权等 Java 实战代码。剖析 JWT 注销、并发刷新、令牌泄露等落地痛","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F739056\u002F202609\u002F739056-20260902114155245-1924311389.png","\u002Fnews\u002F843",[19],{"id":79,"kind":7,"title":80,"summary":81,"image":15,"href":82,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":83},"NEWS_ARTICLE:854","分治：序列分治（CDQ）与点分治","分治：序列分治（CDQ）与点分治 一、分治思想概述 分治（Divide and Conquer）是算法设计中最核心的思想之一。它的基本策略是： 分（Divide）：将原问题划分为规模更小的子问题。 治（Conquer）：递归地求解子问题（若子问题足够小则直接求解）。 合（Combine）：将子问题的","\u002Fnews\u002F854",[19],{"id":85,"kind":7,"title":86,"summary":87,"image":88,"href":89,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":90},"NEWS_ARTICLE:863","弱模型不能裸奔：Agent Harness 凭什么真实有效","Harness 不是给弱模型贴的创可贴。它是把工程纪律——验证、门禁、不变量、路由——变成架构里一等公民的方式。模型每半年换一代，今天省钱的 Flash 明天可能就过时了，但那套'默认怀疑、机器校验、按决策密度调度'的流程会留下来，并且越跑越值钱。\n弱模型不能裸奔。给它穿上 harness，便宜才真","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F510\u002F202608\u002F510-20260830181633785-1171146572.jpg","\u002Fnews\u002F863",[19],{"id":92,"kind":7,"title":93,"summary":94,"image":95,"href":96,"meta":37,"badge":10,"author":12,"stats":-1,"accent":38,"coverRatio":39,"tags":97},"NEWS_ARTICLE:865","焕新鸿蒙应用权限管理方案，应用授权体验再升级","作为用户或应用开发者，或许经历过类似的体验场景：使用应用的过程中，触发应用某些功能会需要访问你的位置、麦克风、相机等常用权限，若为了保护隐私拒绝授权后，想要使用功能时，再次打开却找不到设置入口；或开启流程繁琐，需要经过频繁跳转和设置。这一问题不仅影响用户体验，还可能造成应用功能不可用、用户流失，也制","https:\u002F\u002Fimg2024.cnblogs.com\u002Fblog\u002F2396482\u002F202609\u002F2396482-20260901170248450-744043562.png","\u002Fnews\u002F865",[19]]