Skip to content
C++
Build System
Updated 2025-01-01T00:00:00.000Z

Ninja Build System

Ninja build system — concepts, hand-written build.ninja, CMake + Ninja backend, incremental builds, compile_commands.json, and speed tips.

TL;DR

Ninja is a small, fast build system designed to be generated by higher-level tools like CMake. It beats Make on large C++ projects because it uses a simpler dependency model and runs with maximum parallelism by default.

bash
# Use Ninja as CMake's build backend
cmake -G Ninja -B build
cmake --build build          # parallel by default

Core Concepts

Ninja has four primitives:

PrimitivePurpose
ruleNamed build recipe (how to compile/link)
buildEdge: outputs = rule(inputs)
variableString substitution
poolLimit concurrency for expensive steps

Everything else (.d dependency files, implicit outputs, phony targets) builds on these.


Hand-Written build.ninja

# Variables
cxx = g++
cflags = -std=c++23 -O2 -Wall

# Rules
rule compile
  command = $cxx $cflags -MMD -MF $out.d -c $in -o $out
  depfile = $out.d
  deps = gcc
  description = Compile $in

rule link
  command = $cxx $in -o $out
  description = Link $out

# Build edges
build obj/main.o: compile src/main.cpp
build obj/engine.o: compile src/engine.cpp
build obj/renderer.o: compile src/renderer.cpp

# Link
build app: link obj/main.o obj/engine.o obj/renderer.o

# Default target
default app

Key variables in rules

VariableMeaning
$inSpace-separated list of input files
$outSpace-separated list of output files
$depfilePath to GCC-format dependency file

Automatic Dependency Tracking

The -MMD -MF $out.d flags make the compiler write a .d file listing all #included headers. Ninja reads these with deps = gcc and rebuilds correctly when any header changes — no manual dependency listing needed.

rule cxx_compile
  command = g++ -std=c++23 -MMD -MF $out.d -c $in -o $out
  depfile = $out.d
  deps = gcc        # tells ninja to read and cache the depfile

Phony Targets and Pools

# Phony target (like .PHONY in Make)
build clean: phony
  command = rm -rf obj/ app

build all: phony app tests

# Pool: limit link parallelism (linking is memory-heavy)
pool link_pool
  depth = 2

rule link
  command = $cxx $in -o $out
  pool = link_pool  # at most 2 link jobs at once

Using Ninja with CMake

bash
# Configure with Ninja generator
cmake -G Ninja -B build -DCMAKE_BUILD_TYPE=Release

# Build (Ninja auto-detects CPU count for -j)
cmake --build build

# Build with explicit parallelism
cmake --build build -- -j16

# Clean
cmake --build build --target clean

# List all targets
cmake --build build --target help

CMakeLists.txt — nothing special needed

CMake generates build.ninja automatically. All standard CMake features (targets, dependencies, install) work identically with the Ninja generator.


compile_commands.json

Ninja + CMake generates compile_commands.json for clangd (LSP), clang-tidy, and other tools:

bash
cmake -G Ninja -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# creates build/compile_commands.json

# Symlink to project root for clangd to find it
ln -sf build/compile_commands.json compile_commands.json

Useful ninja Commands

bash
ninja                    # build default target
ninja -j8                # limit to 8 parallel jobs
ninja app tests          # build specific targets
ninja -n                 # dry run — show commands without running
ninja -v                 # verbose: print full commands
ninja -C build           # run from a different directory

# Diagnostics
ninja -t list            # list all targets
ninja -t deps app        # show dependency chain for 'app'
ninja -t compdb          # print compilation database (JSON)
ninja -t graph app | dot -Tpng > deps.png  # visualize dependency graph
ninja -t clean           # remove built outputs
ninja -t recompact       # compact the .ninja_deps database

Incremental Builds

Ninja tracks two kinds of state:

  1. Timestamps — if an output is older than its inputs, rebuild
  2. Dependency database (.ninja_deps) — discovered header dependencies from depfiles

On large projects this makes a "no-op" build (nothing changed) extremely fast — Ninja just stat()s files and exits.

bash
# Force rebuild of a specific target
ninja -t clean app && ninja app

# Show what would be rebuilt
ninja -n

# Check why something is being rebuilt
ninja -d explain app 2>&1 | head -20

Speed Tips

bash
# Use Ninja instead of Make (2-5x faster on large projects)
cmake -G Ninja ...

# Enable link-time parallelism with lld
add_link_options(-fuse-ld=lld)

# Split compilation with unity/jumbo builds (fewer TUs)
set_target_properties(mylib PROPERTIES UNITY_BUILD ON)

# Precompiled headers
target_precompile_headers(myapp PRIVATE src/pch.h)

# Use ccache with Ninja
cmake -G Ninja -DCMAKE_CXX_COMPILER_LAUNCHER=ccache ...

# Distributed builds with distcc or icecc
set(CMAKE_CXX_COMPILER_LAUNCHER "distcc")

Comparison: Ninja vs Make vs Meson

NinjaMakeMeson
RoleBuild runner (generated)Build runner (hand-written or generated)Meta-build (generates Ninja)
ParallelismDefault maxManual -j$(nproc)Default max (via Ninja)
Dependency trackingDepfiles, deps = gcc.d files (manual setup)Automatic
Incremental accuracyHighMedium (timestamp only)High
Human-writableBarelyYesYes
Speed (no-op build)Very fastSlow (recursive make)Very fast (via Ninja)
compile_commands.jsonYes (via CMake flag)With Bear/CMakeBuilt-in

Typical workflow: write CMakeLists.txt or meson.build → generate Ninja files → Ninja executes the build.

Edit on GitHubUpdated 2025-01-01T00:00:00.000Z