Build Systems

Ninja Build System

Ninja was created at Google to fix the slow build times of Chromium — a C++ project with tens of thousands of source files. It is now the default build backend for ESP-IDF, Android, and many large embedded projects.


Why Ninja Exists

When Google engineers tried to build Chromium with make, they hit a specific problem: Make was spending 10–20 seconds just figuring out what needed to be rebuilt — before a single .c file was even touched by the compiler. With a team of hundreds, this wasted enormous amounts of time.

Evan Martin at Google wrote Ninja in 2012 with a single design goal:

Be as fast as possible at determining what needs to be built and building it.

Ninja achieves this by:

  1. Stripping all scripting features from the build file format (no variables, no functions, no conditionals)
  2. Keeping the entire dependency graph in memory after reading it once
  3. Automatically parallelizing all independent jobs across CPU cores

Make vs Ninja — Detailed Comparison

FeatureGNU MakeNinja
Dependency analysis speedSlow (seconds for large projects)Instant (milliseconds)
Parallel buildsManual (make -j8)Automatic (detects CPU count)
Human-readable filesYes — you write MakefilesNo — CMake generates build.ninja
Scripting featuresString functions, conditionals, includesNone
Designed forHuman authoringTool-generated files
Used byOld C/C++ projects, STM32CubeIDEESP-IDF, Android, Chrome, LLVM
InstallPre-installed on most Linuxsudo apt install ninja-build

Installing Ninja

bash
# Ubuntu / Debian / Raspberry Pi OS
sudo apt install ninja-build
ninja --version    # 1.11.x

# macOS
brew install ninja

# Windows
winget install Ninja-build.Ninja
# or download from: https://github.com/ninja-build/ninja/releases

What a build.ninja File Looks Like

Ninja files are machine-generated and not meant to be hand-written. But looking at a real one shows what CMake produces for your project:

ninja
1# build.ninja (generated by CMake — simplified)
2
3# Rules define how to compile/link
4rule CC
5 command = /usr/bin/gcc $DEFINES $INCLUDES $FLAGS -c $in -o $out
6 description = Building C object $out
7
8rule LINK
9 command = /usr/bin/gcc $FLAGS $in -o $out $LINK_LIBRARIES
10 description = Linking $out
11
12# Build statements: what to build, from what, using which rule
13build CMakeFiles/hello.dir/main.c.o: CC /home/user/project/main.c
14 DEFINES = -DESP_PLATFORM
15 INCLUDES = -I/home/user/project/include
16 FLAGS = -Wall -O2
17
18build hello: LINK CMakeFiles/hello.dir/main.c.o
19 LINK_LIBRARIES = -lm

Each build line is one compilation job. Ninja reads all of them at once, builds the dependency graph, identifies which jobs have no interdependencies, and runs them in parallel on every available CPU core.


Using Ninja with CMake (Desktop Project)

bash
mkdir build && cd build

# Tell CMake to generate Ninja files instead of Makefiles
cmake .. -G Ninja

# Build (Ninja auto-detects CPU core count)
ninja

# Build with explicit parallelism
ninja -j8

# Verbose mode — see every compiler command
ninja -v

# Build a specific target only
ninja sensor_lib

# List all available targets
ninja -t targets

# Clean
ninja -t clean

Using Ninja with ESP-IDF

When you run idf.py build, Ninja is what actually compiles your code. You can also invoke it directly:

bash
# Standard way (recommended)
idf.py build

# Direct Ninja invocation — faster for incremental builds
cd build
ninja

# Build only a specific component
ninja esp-idf/components/driver/libdriver.a

# Show what would be built without building
ninja -n

# See full compiler commands
ninja -v 2>&1 | head -30

Ninja Build Statistics

After a build, Ninja reports timing:

text
[137/137] Linking CXX executable firmware.elf
  137 jobs completed, 0 failed.
  Build completed in 12.4s

Using Ninja with STM32 (CMake + ARM toolchain)

bash
mkdir build && cd build

cmake .. \
  -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=../arm-none-eabi-toolchain.cmake \
  -DCMAKE_BUILD_TYPE=Release

# Build firmware
ninja

# Output:
# [1/45] Building C object Core/Src/main.c.o
# [2/45] Building C object Drivers/HAL/stm32f4xx_hal.c.o
# ...
# [45/45] Linking firmware.elf
# arm-none-eabi-size firmware.elf:
#    text    data     bss     dec     hex filename
#   18432     512    1024   19968    4e00 firmware.elf

# Flash
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg \
        -c "program firmware.bin verify reset exit 0x08000000"

The ESP-IDF Build Directory Structure

After idf.py build, the build/ directory contains:

text
build/
├── build.ninja               ← Ninja's master build file
├── CMakeCache.txt            ← CMake's cached configuration
├── compile_commands.json     ← Compile commands for VS Code IntelliSense
├── project_name.elf          ← Linked firmware (with debug symbols)
├── project_name.bin          ← Flashable binary
├── project_name.map          ← Linker map (shows flash/RAM usage)
└── esp-idf/
    └── components/
        └── freertos/
            └── libfreertos.a ← Pre-compiled component libraries

The compile_commands.json file is particularly useful — VS Code reads it to provide accurate IntelliSense, auto-complete, and error highlighting for your ESP32 project.


Ninja Performance Tips

bash
# Use all cores (Ninja does this automatically, but you can be explicit)
ninja -j$(nproc)

# If your machine has limited RAM, reduce parallel jobs
ninja -j2

# Speed up repeated builds with ccache (compiler cache)
sudo apt install ccache
export CC="ccache gcc"
export CXX="ccache g++"
cmake .. -G Ninja -DCMAKE_C_COMPILER_LAUNCHER=ccache
ninja    # First build: normal speed. Second build: 10x faster.

Summary

When to useTool
Writing a quick one-file programgcc main.c -o app directly
Small multi-file desktop projectMakefile
Cross-platform or MCU projectCMake (generates Ninja files)
ESP32 firmwareidf.py build (CMake + Ninja)
STM32 from IDESTM32CubeIDE (CMake + Make internally)
STM32 from CLIcmake -G Ninja + ninja
Large project, fastest rebuildNinja + ccache
Previous
CMake Basics