Ninja 一般作為底層的 bulid executor 使用。使用其他更高階的 build system 來產生 build.ninja
檔案,再用 ninja
來執行 build。舉例來說 LLVM 使用的 cmake
、systemd 使用的 meson
、chromium 使用的 gn
都可以產生 ninja
的 build file。
不過這篇要來介紹怎麼手寫 ninja
以及它的功能同時撻伐 make。
基本語法
首先看稍微修改的官網的 example:
cflags = -Wall
rule cc
command = gcc $cflags -c $in -o $out
build foo.o: cc foo.c
build bar.o: cc bar.c
cflags = -Wall
將 cflags
變數設為 -Wall
。之後 $cflags
就會擴展成 -Wall
rule cc
定義一個新的 rule 名為 cc
。cc
這個名稱可以自己取。
command = gcc $cflags -c $in -o $out
在 cc 這個 rule 之下,將 command 變數設為 gcc $cflags -c $in -o $out
。$in
、$out
分別為自動代入的輸入以及輸出檔案。
build foo.o: cc foo.c
要求 ninja 以 cc
這個 rule
來建構。其中 foo.c
為 $in
,foo.o
為 $out
。
當 ninja
發現 $in
的內容改變的時候,就會重新執行 cc
中的 $command
來產生新的 $out
,也就是會執行:gcc -Wall -c foo.c -o foo.o
。
build bar.o: cc bar.c
跟上面類似,執行 gcc -Wall -c bar.c -o bar.o
。
功能
寫完 rule 之後來執行看看:
% ninja
[2/2] gcc -Wall -c bar.c -o bar.o
除了把東西編出來以外,ninja 還幫我們做了什麼?
更新偵測
Input 沒變的話不會重新編譯。這點跟 make
是一樣的。幾乎是 build system 的基本需求。
% ninja
[2/2] gcc -Wall -c bar.c -o bar.o
% ninja
ninja: no work to do.
% touch foo.c
% ninja
[1/1] gcc -Wall -c foo.c -o foo.o
內建 clean tool
不需要額外自己寫 clean rule。
% ninja -t clean
Cleaning... 2 files.
自動平行
ninja
會自行偵測系統中的 CPU 數量作為預設的平行參數。
% ninja --help
[...]
-j N run N jobs in parallel (0 means infinity) [default=10 on this system]
make
相較之下預設是 -j1
。不加數字直接 -j
是有多少 job 能跑就瘋狂跑。引用 GNU Make 的文件:If there is nothing looking like an integer after the ‘-j’ option, there is no limit on the number of job slots。
Compilation Database
可自動產生 compilation database 供 clangd 或 IDE 使用(編輯器需要知道每個 source file 的編譯參數才能非常正確的運作)。
% ninja -t compdb
[
{
"directory": "/tmp/ninja1",
"command": "gcc -Wall -c foo.c -o foo.o",
"file": "foo.c",
"output": "foo.o"
},
{
"directory": "/tmp/ninja1",
"command": "gcc -Wall -c bar.c -o bar.o",
"file": "bar.c",
"output": "bar.o"
}
]
一般的用法會是:
% ninja -t compdb > compile_commands.json
進度、防洗版
內建就有進度了 [1/2]
[2/2]
。此外沒有錯誤的時候不會把 terminal 洗掉。
如果出現 warning 或是 error 也會把 compile command 跟錯誤訊息整理好,每個 warning/error message 對應到的 command 是什麼直接往上找就好了,不會有多個 command 交錯的情形。
% ninja
[1/2] gcc -Wall -c bar.c -o bar.o
FAILED: bar.o
gcc -Wall -c bar.c -o bar.o
bar.c:5:1: error: expected identifier or ‘(’ at end of input
5 | }
| ^
[2/2] gcc -Wall -c foo.c -o foo.o
foo.c: In function ‘main’:
foo.c:2:13: warning: unused variable ‘a’ [-Wunused-variable]
2 | int a;
| ^
ninja: build stopped: subcommand failed
反觀…
% make -j2
gcc -Wall -c -o foo.o foo.c
gcc -Wall -c -o bar.o bar.c
foo.c: In function ‘main’:
foo.c:2:13: warning: unused variable ‘a’ [-Wunused-variable]
2 | int a;
| ^
bar.c:5:1: error: expected identifier or ‘(’ at end of input
5 | }
| ^
make: *** [<builtin>: bar.o] Error 1
make: *** Waiting for unfinished jobs....
一兩個檔案可能還好,但是如果是數十個檔案或是數百個檔案而且 -j64
的時候會很崩潰。
Build Command Dependency
修改 build.ninja
以後,如果造成某個 output file 的 build command 改變,會自動重編該檔案。
其他功能
追蹤 Header File 更新
Ninja 能夠利用 gcc
/ clang
的 dependency file 的功能來自動偵測 header file 的改變來重編 source file。參考資料
直接舉例:
% cat build.ninja
rule cc
depfile = $out.d
command = gcc -MD -MF $out.d -c $in -o $out
build foo.o: cc foo.c
% cat foo.c
#include "foo.h"
% ninja
[1/1] gcc -MD -MF foo.o.d -c foo.c -o foo.o
此時產生了兩個檔案,一為 foo.o
,另外一個為 foo.o.d
。
% cat foo.o.d
foo.o: foo.c /usr/include/stdc-predef.h foo.h
接著 touch foo.h
即會觸發 foo.o
重編:
% touch foo.h
% ninja
[1/1] gcc -MD -MF foo.o.d -c foo.c -o foo.o
ninja
自動透過 foo.o.d
來知道 foo.c
有 #include "foo.h"
。
% ninja -t clean
Cleaning... 2 files.
% ls
build.ninja foo.c foo.h
此外,clean
的時候,foo.o
跟 foo.o.d
會被當成 output 一起清掉。
Build Graph
我們可以利用 ninja -t graph
來輸出 build graph,檢查自己 build.ninja
有沒有寫壞,像是檔名有奇怪的字元、或是 dependency 很複雜的情形。
以下方 build.ninja
為例:
rule cc
command = gcc -o $out -c $in
rule link
command = gcc -o $out $in
build main.o: cc main.c
build white$ space.o: cc white$ space.c
build a.out: link white$ space.o main.o
執行以下指令以及輸出的 build graph 如下(dot
為 graphviz 的指令):
ninja -t graph | dot -Tsvg -ograph.svg
多重 Output File
build
到 :
中間寫多個檔案就能支援有多個 output file。
rule protoc
command = protoc --proto_path=. --cpp_out=. $in
build baz.pb.h baz.pb.cc: protoc baz.proto
程式產生 ninja.build
官方有出一個 ninja_syntax.py。可以方便使用 Python 產生 ninja.build
。
心得
ninja
做小專案很方便。更複雜的專案個人不會想手寫 Makefile
。