Ninja构建系统
Ninja构建系统
ninja 美 [ˈnɪndʒə],英 ['nɪndʒə] n. 忍者(受过日本传统打斗和轻功训练的人)
官网
ninja-build.org
Github
github.com/ninja-build/ninja
ninja简单介绍
ninja是一种构建工具,用来调用各种工具(代码生成器、编译器、链接器等)来编译大型项目
与cmake/make等工具不同,ninja在设计之初就不是给人写的,只是用于作为其它程序生成的目标。
ninja的设计哲学就是避免任何模糊的内容
比如makefile里面可能经常会用dir/*.cpp来表示源文件
但是这需要查询文件系统才能得到具体内容,是一个很慢而且很不确定的操作
ninja把这些模糊的操作都交给元构建系统(meta-build system,例如cmake),只留下真正需要编译的命令。
因此,本质上我们可以认为ninja就是一条一条地列出了具体要执行的命令,然后执行即可。
ninja会分析这一系列命令之间的依赖关系,并根据这个依赖关系实现两个重要特点:
并行编译
没有依赖的命令可以并行执行。ninja默认使用的并行数为CPU数量,一般不用手动设置并行数,除非想限制ninja使用的CPU数量(例如有其他任务在运行,只让ninja用一半CPU)
增量编译
根据文件的时间戳进行分析,如果某个文件的时间戳发生了改变,则依赖于这个文件的命令以及其他依赖于这个命令的命令都会被重新执行,以此达到增量编译的效果(比如修改了一个文件之后重新编译)
ninja 安装方式
ninja体量很小
三种安装方式
系统级安装
Ubuntu 的
apt-get install ninja-build
MacOS的
brew install ninja
安装后ninja就是一个系统命令,类似ls/cat等命令
用户级安装
conda都可以安装ninja
conda install ninja
通过pip
pip install ninja
自定义安装
在GitHub下载页面下载安装包并解压即可
或者下载源码自己编译
ninja 构建配置文件build.ninja
例子
有两个头文件a1.h和a2.h
分别定义了一个变量a
但是有不同的值
// a1.h
int a = 1;
// a2.h
int a = 2;
源文件a.cpp
里根据宏的不同来包含这两个文件中的某一个
// a.cpp
#include<iostream>
using namespace ::std;
#ifdef USE_A1
#include "a1.h"
#endif
#ifdef USE_A2
#include "a2.h"
#endif
int main()
{
cout << "hello, world from " << a << "!\n";
}
一份
build.ninja文件
包含ninja的构建配置(注意最后的换行,是必要的)
# build.ninja
cxxflags = -DUSE_A1
rule compile
description = compile with $cxxflags
command = g++ -MM -MF out in && g++ in -o $out
depfile = $out.d
build compile_with_default_cxxflags: compile a.cpp
build compile_with_shadow_cxxflags: compile a.cpp
cxxflags = -DUSE_A2
default compile_with_default_cxxflags
配置里包含了内容
注释(comment)
build.ninja里用#开头的行表示注释
变量(variable)
cxxflags = -DUSE_A1
定义了一个变量 cxxflags
其值为-DUSE_A1
变量的值在后面的代码中都可以用
引用方式为 $cxxflags
-DUSE_A1
就是在编译之前定义名为 USE_A1 的宏
规则(rule)
语法为rule 加上规则的名字
定义了一个名为 compile 的规则
规则内有默认变量out
$in 表示输入文件的列表
$out 表示输出文件名
规则的文字描述(description)
用于在编译过程中展示并告诉用户目前在干什么
规则的具体命令(command)
写明具体的编译命令
规则的依赖项记录文件(depfile)
用于增量编译,详见后面的解释
构建(build)
语法为build加上构建的文件名,加上冒号:以及规则名、输入文件列表
本质上
build rule
in和rule
然后运行它的command命令
构建里的局部变量
在build下面的任意变量
在本次构建命令中覆盖全局的变量
build compile_with_shadow_cxxflags 里定义的cxxflags会覆盖本次compile命令用到的cxxflags
默认构建目标(default)。可以存在多行default,最后的默认构建目标就是它们的并集。如果没有任何default,则默认构建全部的build条目。
有了 build.ninja 通过ninja或者ninja compile_with_default_cxxflags来构建默认的代码,得到可执行文件compile_with_default_cxxflags(执行后输出hello, world from 1!);也可以通过ninja compile_with_shadow_cxxflags得到可执行文件compile_with_shadow_cxxflags(执行后输出hello, world from 2!)。我们也可以修改a1.h或者a2.h,观察ninja是否会重新编译。
理解ninja的增量编译
build.ninja 大部分内容简单易懂,唯一难以理解的就是用于增量编译的
depfile = $out.d
$out是ninja的默认变量
表示构建的目标文件名
所以$out.d就是目标文件名加上.d 比如 build compile_with_default_cxxflags
depfile 就是 compile_with_default_cxxflags.d
然后需要理解增量构建的目标:
build compile_with_default_cxxflags 构建条目中
只传入了 a.cpp 这个文件
但是实际上代码里面包含了 a1.h 或 a2.h
如何能够让ninja知道增量构建的时候需要检查a1.h或者a2.h
这就需要编译器的支持了
g++ 的 -MM -MF out.d中
下次ninja就会通过这个文件的内容来判断具体需要检查发生改变的文件
例如,compile_with_default_cxxflags.d文件里,就有以下内容
compile_with_default_cxxflags: a.cpp a1.h
下次ninja再build compile_with_default_cxxflags
的时候,就会检查这个文件,然后查看a.cpp与a1.h的修改时间,
如果任何一个文件被修改了,就重新编译
另外,如果compile_with_default_cxxflags文件不在了,ninja也会重新编译(这个很好理解)
ninja进阶用法
很多ninja.build里存在phony规则
这是一个内置规则,意思是“假冒的”,就是一个不存在规则的规则
rule phony
command = : out
其中冒号
:是unix系统里的一个命令 不管参数是什么,永远正常退出
因此,phony 规则不会做什么 但是会在输入和输出之间建立依赖关系
不带参数的ninja命令会构建文件里的default构建目标
也可以用ninja compile_with_shadow_cxxflags来手动构建某个目标
构建多个目标(比如PyTorch有上千个目标)的时候
ninja会展示一个进度条
进度条展现的内容就是rule下面的description字段的内容
ninja 高级用法一般都在ninja -t下面
例如
ninja -t clean
清理全部生成文件
ninja -t browse
打开一个网页浏览器查看文件之间的依赖图(默认查看的是名为all的依赖图)
通过浏览器界面,方便查看什么文件依赖于什么文件
打开
http://localhost:8000/?libtorch.so
就可以看到完整的libtorch.so对应的依赖
再也不用对着CMakeLists.txt瞎猜了
还可以通过ninja -t graph直接导出依赖图的dot graph文件
但是大型项目的依赖图一般都非常复杂,看起来不方便
ninja -t targets all
得到的是全部的构建目标
用于grep搜索想要的内容;
ninja -C /path/to/dir -f /path/to/file
让ninja切换到/path/to/dir去执行命令
并读取/path/to/file配置文件来执行
-f的默认参数是build.ninja
-C的默认参数为.,
也就是当前路径
一般来说
-f参数用得很少
-C参数用得比较多