Build Tools (Part 9): Ninja — The Flash of the Build World

1. Isn’t make Fast Enough?

After a few years of using make, have you ever noticed this phenomenon:

make -j8

You have 8 parallel jobs running, the CPU is saturated, and yet make still “hangs” for a few seconds at startup before it really starts compiling.

What is make doing during those few seconds?

It is building the dependency graph — parsing the Makefile to work out who depends on whom, and who gets compiled before whom. A Makefile is a text format, so make re-parses it from scratch on every startup. The bigger the project, the longer that parsing takes.

What’s more, make’s reasoning about the dependency graph is rather conservative — even when there is no ordering dependency between A and B, make often runs them one after the other, because it has no global view.

Ninja goes straight at both of these pain points: faster startup, more aggressive parallelism.

2. Ninja’s Design Philosophy

Ninja’s slogan is refreshingly plain: “dedicated to making your compiles faster”.

Its design idea is simple:

  • Build files are not meant to be written by people; they are generated by programs. Ninja’s .ninja file format is extremely lean, with almost no syntactic sugar
  • The codebase is tiny, so parsing is fast enough to be essentially negligible. The few seconds make spends “staring into space” become milliseconds in Ninja
  • Its parallelism decisions are more precise, making the most of a multi-core CPU

Ninja is not positioned to replace CMake but to replace make. You can think of Ninja as a back-end engine that “starts builds faster and runs them faster”.

CMakeLists.txt → cmake -G Ninja → build.ninja → ninja runs

3. Generating Ninja Build Files with CMake

There is almost nothing extra to learn. You only have to pick a different generator when you run cmake:

mkdir build && cd build
cmake .. -G Ninja
ninja

Exactly the same usage as cmake .. && make. Not a single line of CMakeLists.txt needs changing.

If you haven’t installed Ninja yet:

# Ubuntu / Debian
sudo apt install ninja-build

# macOS
brew install ninja

Once it is running you feel the difference immediately — Ninja barely pauses at all; it starts compiling right away.

4. What build.ninja Looks Like

You normally do not need to write .ninja files directly, but a look at the structure helps explain why it is so fast:

# Variables
cflags = -Wall -g

# rule: defines one way of "how to compile"
rule cc
  command = gcc $cflags -c $in -o $out
  description = CC $out

# build: describes "which rule, from what, produces what"
build main.o: cc main.c
build utils.o: cc utils.c

# Linking rule
rule link
  command = gcc $in -o $out
  description = LINK $out

build myapp: link main.o utils.o

# Default target
default myapp

A rule defines the template for a compiler command; a build statement declares a concrete compilation task and its dependencies. The format is extremely lean, with nothing fancy in it — which is exactly why parsing is so fast.

5. Ninja vs Make: A Speed Comparison

Take a medium-sized C project (about 500 source files) and a pure incremental build (changing nothing at all):

# make
time make -j8
# real 0m3.2s

# ninja
time ninja
# real 0m0.3s

Ninja’s incremental build (no-op build) is nearly 10 times faster than make. Accumulated over a day of repeatedly editing, compiling and testing, those 3 seconds save you a lot of time.

For a full build the gap is not that large, but Ninja’s advantage in parallel scheduling still makes it a little faster. With a caching tool such as ccache, Ninja’s no-op advantage becomes even more obvious.

6. Ninja’s Limitations

Fast as it is, Ninja is not a universal solution:

  • Not suited to being written by hand: .ninja files are extremely tedious. You will never want to write one yourself
  • It needs a high-level build system alongside it: Ninja is only an execution engine; you still need CMake (or GN, Meson) to generate the build files
  • No conditionals, functions or other high-level features: those are the generator’s (CMake’s) responsibility; Ninja only executes

You can think of it this way: make is both a build description language and a build execution engine. Ninja separates the two roles — you describe the build logic with CMake and Ninja executes it, each minding its own business.

Modern large C++ projects (Chromium, LLVM) almost all use Ninja as their build back end.

7. When to Use Ninja

Scenario Recommendation
Small project (< 10 source files) make is enough; no need to bring in Ninja
Medium project (10-100 source files) Ninja’s startup speed is noticeable, but the difference is small
Large project (100+ source files) Ninja is strongly recommended — the time saved is very substantial
Continuous integration (CI) Ninja is a must — a CI environment builds from scratch every time
Used together with CMake Switching costs almost nothing: cmake -G Ninja gets you there in one step

If you are already using CMake, switching to Ninja costs you almost nothing — change one generator argument and you get faster builds. Why not?


In the next article we will meet a heavyweight build system from Google — Bazel. It is said to handle monorepo projects with tens of thousands of source files; what design philosophy is behind it?

A small step forward every day is a new height!

Author: Tang Moying

Source: /en/post/build-09-ninja

License: this article is shared under "CC BY 4.0". Please credit the author and link back to the original when republishing.