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:
- Stripping all scripting features from the build file format (no variables, no functions, no conditionals)
- Keeping the entire dependency graph in memory after reading it once
- Automatically parallelizing all independent jobs across CPU cores
Make vs Ninja — Detailed Comparison
| Feature | GNU Make | Ninja |
|---|---|---|
| Dependency analysis speed | Slow (seconds for large projects) | Instant (milliseconds) |
| Parallel builds | Manual (make -j8) | Automatic (detects CPU count) |
| Human-readable files | Yes — you write Makefiles | No — CMake generates build.ninja |
| Scripting features | String functions, conditionals, includes | None |
| Designed for | Human authoring | Tool-generated files |
| Used by | Old C/C++ projects, STM32CubeIDE | ESP-IDF, Android, Chrome, LLVM |
| Install | Pre-installed on most Linux | sudo apt install ninja-build |
Installing Ninja
# 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:
1# build.ninja (generated by CMake — simplified)23# Rules define how to compile/link4rule CC5 command = /usr/bin/gcc $DEFINES $INCLUDES $FLAGS -c $in -o $out6 description = Building C object $out78rule LINK9 command = /usr/bin/gcc $FLAGS $in -o $out $LINK_LIBRARIES10 description = Linking $out1112# Build statements: what to build, from what, using which rule13build CMakeFiles/hello.dir/main.c.o: CC /home/user/project/main.c14 DEFINES = -DESP_PLATFORM15 INCLUDES = -I/home/user/project/include16 FLAGS = -Wall -O21718build hello: LINK CMakeFiles/hello.dir/main.c.o19 LINK_LIBRARIES = -lmEach 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)
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:
# 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:
[137/137] Linking CXX executable firmware.elf
137 jobs completed, 0 failed.
Build completed in 12.4s
Using Ninja with STM32 (CMake + ARM toolchain)
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:
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
# 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 use | Tool |
|---|---|
| Writing a quick one-file program | gcc main.c -o app directly |
| Small multi-file desktop project | Makefile |
| Cross-platform or MCU project | CMake (generates Ninja files) |
| ESP32 firmware | idf.py build (CMake + Ninja) |
| STM32 from IDE | STM32CubeIDE (CMake + Make internally) |
| STM32 from CLI | cmake -G Ninja + ninja |
| Large project, fastest rebuild | Ninja + ccache |

