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 名為 cccc 這個名稱可以自己取。

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$infoo.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 databaseclangd 或 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 / clangdependency 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.ofoo.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 如下(dotgraphviz 的指令):

ninja -t graph | dot -Tsvg -ograph.svg

graph.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