CMake vs Make vs Ninja vs Meson: What Each Build Tool Actually Does and How to Choose
Key takeaways
Make and Ninja execute builds while CMake and Meson generate build files for them, which is why they are usually combined rather than chosen alone. The post shows each tool's syntax, why Ninja feels faster (default parallelism and cheap no-op builds, not faster compiling), the dependency-tracking traps of hand-written Makefiles, and pairings by project size and platform.
Introduction: What is a Build System?
When starting a C++ project, you run into CMake, Make, Ninja and Meson almost at once, and it is not obvious that they sit at different layers: CMake and Meson generate build files, while Make and Ninja execute them. That is why “CMake or Ninja?” is not really a choice; most CMake projects are built with Ninja. This article explains what a build system does, shows each tool with a minimal example and its trade-offs, and ends with the combinations that fit small, medium and large projects.
What is a Build System?
Role of Build System
A build system automates turning source code into executables and libraries. Compiling a single file by hand is easy; the build system’s job is everything around it.
Build Process:
flowchart LR
A[Source Code] --> B[Preprocessing]
B --> C[Compilation]
C --> D[Linking]
D --> E[Executable]
F[Build System] -.Manages.-> A
F -.Manages.-> B
F -.Manages.-> C
F -.Manages.-> D
What Build Systems Do:
- Dependency tracking: know which outputs depend on which inputs, including headers
- Incremental builds: rebuild only what is out of date
- Parallel builds: run independent compile steps at the same time
- Portability (generators only): produce the right commands and flags for each platform and compiler
The second point is where build systems earn their keep and where hand-rolled ones break. A .cpp file depends on every header it includes, directly or indirectly. If the build system does not know that, changing a header rebuilds nothing, and you link object files compiled against the old definition of a struct. The result is not a build error but a program that crashes or behaves strangely, because two translation units disagree about a type’s layout.
Build Tool vs Build File Generator
Build tool: reads a build file, compares timestamps (or hashes) of inputs and outputs, and runs the compiler for what is out of date. Examples: Make, Ninja.
Build file generator: reads a higher-level project description and writes build files for a build tool, filling in compiler flags, platform details and dependency information. Examples: CMake, Meson.
Relationship:
CMakeLists.txt (CMake script)
↓ Run CMake
Makefile or build.ninja (build file)
↓ Run Make or Ninja
Executable
Make
What is Make?
Make was created in 1976 by Stuart Feldman at Bell Labs and is still the default build tool on Unix-like systems. GNU Make is the most common implementation; BSD make and Microsoft’s nmake are different dialects with incompatible extensions.
Makefile Example
# Makefile
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O2
# Target: Dependencies
# Command
all: main
main: main.o utils.o
$(CXX) $(CXXFLAGS) -o main main.o utils.o
main.o: main.cpp utils.h
$(CXX) $(CXXFLAGS) -c main.cpp
utils.o: utils.cpp utils.h
$(CXX) $(CXXFLAGS) -c utils.cpp
clean:
rm -f *.o main
.PHONY: all clean
Usage:
# Build
make
# Parallel build (4 jobs simultaneously)
make -j4
# Clean build
make clean && make
Each rule says “this target depends on these files; if any is newer than the target, run these commands”. The header dependencies (utils.h) are written by hand here, and that is the weak point: add #include "config.h" to utils.cpp and forget to update the Makefile, and changes to config.h are silently ignored. The standard fix is to let the compiler write the dependencies: add -MMD -MP to CXXFLAGS and -include $(OBJS:.o=.d) at the end of the Makefile. GCC and Clang then produce a .d file per object listing every header it used.
The other classic error is Makefile:5: *** missing separator. Stop., which almost always means a recipe line is indented with spaces instead of a tab. Many editors convert tabs to spaces by default, which makes this a common first-day problem.
Make Pros and Cons
Pros:
- Simplicity: the core idea (targets, prerequisites, commands) fits on a page
- Availability: preinstalled or one package away on every Unix-like system
- Flexibility: any shell command can be a build step, so it also works for documentation, code generation and deployment tasks
Cons:
- Platform-bound: recipes are shell commands, so a Makefile written for Linux (
rm -f,/paths) does not run under Windowscmd; GNU Make works on Windows through MSYS2 or WSL, not natively with MSVC - Manual dependency management: header dependencies must be written by hand or generated with
-MMD - Serial by default: without
-j, Make runs one job at a time - Tricky syntax: tab-sensitive recipes, several kinds of variable assignment (
=,:=,?=), and implicit rules that are easy to trigger by accident
CMake
What is CMake?
CMake is a cross-platform build file generator. From one CMakeLists.txt, it can generate Makefiles, Ninja files, Visual Studio solutions or Xcode projects, and it handles compiler detection, header dependency tracking and library discovery.
CMakeLists.txt Example
cmake_minimum_required(VERSION 3.15)
project(MyProject VERSION 1.0)
# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Create executable
add_executable(main
main.cpp
utils.cpp
)
# Header file path
target_include_directories(main PRIVATE include)
# Compile options
target_compile_options(main PRIVATE
-Wall -Wextra -O2
)
# Link library
find_package(Boost REQUIRED)
target_link_libraries(main PRIVATE Boost::boost)
Usage:
# Create build directory
mkdir build && cd build
# Run CMake (generate Makefile)
cmake ..
# Build
cmake --build .
# Or use Ninja
cmake -G Ninja ..
ninja
Two lines in this example are simplifications. -Wall -Wextra are GCC/Clang flags; MSVC does not understand -Wextra, so a portable project wraps them in $<$<CXX_COMPILER_ID:GNU,Clang>:...> generator expressions. And -O2 is better left to the build type (-DCMAKE_BUILD_TYPE=Release), which already adds optimization flags; hard-coding it also optimizes debug builds, which makes stepping through code in a debugger confusing. The modern equivalent of the last three commands is cmake -S . -B build -G Ninja followed by cmake --build build, which does not require changing directories.
Once CMake has generated a build directory, the generator is fixed for it. Running cmake -G Ninja .. in a directory that was configured with Makefiles fails with Error: generator : Ninja Does not match the generator used previously: Unix Makefiles; use a fresh build directory, or delete CMakeCache.txt and CMakeFiles/.
CMake Pros and Cons
Pros:
- Cross-platform: one description for Windows, macOS and Linux, and for GCC, Clang and MSVC
- Automatic dependency tracking: header dependencies are handled by the generated build files
- Ecosystem: most C++ libraries ship CMake support, and vcpkg and Conan integrate with it
- IDE integration: Visual Studio, CLion, VS Code and Qt Creator open CMake projects directly
Cons:
- Complex language: string-based, with scoping rules and generator expressions that take time to learn
- Configure step: re-running CMake on a large project takes noticeable time, and it re-runs automatically whenever a
CMakeLists.txtchanges - Old tutorials: much of what is online uses pre-3.0 style (
include_directories, global flags), which still works but scales badly
CMake Build Flow
flowchart LR
A[CMakeLists.txt] --> B[Run cmake]
B --> C{Select Generator}
C -->|Unix Makefiles| D[Makefile]
C -->|Ninja| E[build.ninja]
C -->|Visual Studio| F[.sln/.vcxproj]
D --> G[make]
E --> H[ninja]
F --> I[msbuild]
G --> J[Executable]
H --> J
I --> J
Ninja
What is Ninja?
Ninja is a small build tool written for Chromium, designed to be generated rather than written by hand. Its files contain no conditions, loops or functions, only rules and edges, so it can load them and decide what to rebuild very quickly.
build.ninja Example
# build.ninja (usually auto-generated by CMake)
cxx = g++
cxxflags = -std=c++17 -Wall -Wextra -O2
rule cxx
command = $cxx $cxxflags -c $in -o $out
description = CXX $out
rule link
command = $cxx $in -o $out
description = LINK $out
build main.o: cxx main.cpp
build utils.o: cxx utils.cpp
build main: link main.o utils.o
Usage:
# Generate Ninja build files with CMake
cmake -G Ninja ..
# Build with Ninja
ninja
# Explicit job count
ninja -j 8
This handwritten file has the same header problem as the Makefile: nothing says that main.o depends on utils.h. Generated Ninja files solve it with depfile = $out.d and deps = gcc on the compile rule, which make Ninja read the compiler’s dependency output and store it in its own compact database (.ninja_deps). That database is one reason Ninja’s no-op builds are fast: it does not have to parse thousands of .d files on each run.
Ninja Pros and Cons
Pros:
- Fast decisions: checking a large project for changes takes a fraction of a second, so no-op and one-file rebuilds feel instant
- Parallel by default: runs roughly as many jobs as CPU cores without any flag
- Readable output: one status line per step, and the full command only when a step fails
- Minimal features: nothing to misconfigure
Cons:
- Not meant for hand-writing: no variables beyond simple substitution, no conditionals
- Needs a generator: in practice CMake, Meson or GN
- Heavy link steps in parallel: with many large link jobs at once, memory can run out; CMake’s
JOB_POOL_LINKproperty limits concurrent links
Where Ninja’s speed comes from
It is tempting to read “Ninja is faster” as “Ninja compiles faster”. It does not: the compiler does the same work either way, and a clean build of a large project is dominated by compile time. What differs is:
- Default parallelism.
makewithout-jruns one job at a time, whileninjauses all cores. Many “Make is slow” comparisons are reallymakeversusninja -j <cores>. Withmake -j$(nproc), clean build times are usually close. - No-op and incremental builds. When nothing or one file changed, the build tool’s own overhead (reading build files, checking timestamps, resolving dependencies) is the whole cost. In a project with thousands of files and recursive Makefiles, that overhead can be several seconds; Ninja’s single precomputed graph makes it close to zero. This is the difference developers notice dozens of times a day.
- Accurate scheduling. Ninja sees the whole dependency graph at once, while recursive Make invocations (one Makefile per directory) cannot run work from different directories in parallel as effectively.
So the practical advice is simple: when you use CMake, pass -G Ninja (or set it in CMakePresets.json), and if you must use Make, always pass -j.
Meson
What is Meson?
Meson is a build file generator written in Python, with its own small, deliberately non-Turing-complete language. It generates Ninja files on all platforms (and Visual Studio or Xcode projects when asked) and has built-in support for dependencies through pkg-config and its WrapDB of packaged dependencies.
meson.build Example
# meson.build
project('myproject', 'cpp',
version: '1.0',
default_options: ['cpp_std=c++17']
)
boost_dep = dependency('boost')
# Executable
executable('main',
'main.cpp',
'utils.cpp',
include_directories: include_directories('include'),
dependencies: boost_dep
)
Usage:
# Setup build directory
meson setup build
# Build
meson compile -C build
# Or
cd build
ninja
The syntax looks like Python, but it is not: there are no user-defined functions and no arbitrary loops over the file system, which is intentional. A meson.build file cannot do much besides describe targets, so it is easier to read than a large CMake file, and it cannot grow into a program of its own. The trade-off appears when a project needs something unusual; the escape hatch is a separate script called with run_command or custom_target. Target names must be unique, so defining the same executable('main', ...) twice fails with an error that a target of that name already exists.
Meson Pros and Cons
Pros:
- Fast by design: generates Ninja files, and the configure step is quick
- Concise, restricted syntax: easy to read and hard to abuse
- Sensible defaults: warnings, build types, sanitizers (
-Db_sanitize=address) and unity builds are built-in options - Cross-compilation: described with a separate cross file, cleanly separated from the project
Cons:
- Smaller ecosystem: fewer third-party C++ libraries ship
meson.buildfiles, so dependencies often come from pkg-config, WrapDB or a CMake subproject - Fewer learning resources than CMake, especially for large C++ codebases
- IDE support is improving but less universal than CMake’s
Comparison and Selection Guide
Comprehensive Comparison Table
| Feature | Make | CMake | Ninja | Meson |
|---|---|---|---|---|
| Type | Build tool | Generator | Build tool | Generator |
| Portability | Unix-centric | Windows, macOS, Linux | Windows, macOS, Linux | Windows, macOS, Linux |
| Written by hand? | Yes | Yes | No (generated) | Yes |
| Header dependencies | Manual or -MMD | Automatic | From compiler depfiles | Automatic |
| Parallel by default | No (-j) | Depends on generator | Yes | Yes (via Ninja) |
| Learning curve | Low to start, high at scale | High | Low (rarely needed) | Low to medium |
| Ecosystem | Universal on Unix | Largest for C++ | Backend for others | Smaller |
| IDE integration | Limited | Excellent | Via generator | Medium |
There is no universal speed ranking in this table on purpose. Build time depends on the compiler, the project structure, parallelism and caching (ccache, sccache) far more than on the choice of build tool, and published benchmarks rarely transfer from one codebase to another. For your own project, measure a clean build and a one-file incremental build with the combinations you are considering; that takes a few minutes and answers the question for your code.
Selection Flowchart
flowchart TD
A[Start Project] --> B{Cross-platform?}
B -->|Yes| C{Build Speed Important?}
B -->|No| D[Make]
C -->|Yes| E[CMake + Ninja]
C -->|No| F[CMake + Make]
G[New Project?] --> H{Prefer Modern Syntax?}
H -->|Yes| I[Meson + Ninja]
H -->|No| F
Recommended Tools by Scenario
1. Small project or exercise (a handful of files, Unix only)
- Make, with
-MMD -MPfor headers. Enough for algorithm practice and small utilities.
2. Library that others will use
- CMake, because consumers expect
find_packageoradd_subdirectoryto work, and package managers assume it.
3. Large application
- CMake + Ninja, plus a compiler cache. The incremental-build speed is what matters day to day.
4. New project without legacy constraints
- Meson + Ninja is a clean option, particularly for Linux desktop and system software; CMake + Ninja remains the safer choice if you depend on many third-party C++ libraries.
5. Windows only
- Visual Studio projects are simplest if everyone uses Visual Studio, but CMake keeps the door open for other platforms and CI, and Visual Studio opens CMake projects directly.
Real-World Examples
Large projects using CMake: LLVM/Clang, Qt 6, OpenCV, KDE.
Projects built with Ninja: Chromium (generated by GN), Android’s platform build (generated by Soong and Kati), LLVM (generated by CMake).
Projects using Meson: systemd, GStreamer, Mesa, GNOME applications.
Many projects that people cite as “using CMake” or “using Ninja” use both, which reflects the layering: the generator describes the project, Ninja executes it.
Summary
Key Summary
- Make: the oldest build tool, simple for small Unix projects; write header dependencies with
-MMD, and always build with-j. - CMake: the de facto standard generator for C++; complex, but supported by nearly every library, IDE and package manager.
- Ninja: a fast executor for generated build files; the default backend worth choosing for CMake and the only one for Meson.
- Meson: a concise, restricted generator with good defaults and a smaller ecosystem.
Practical Recommended Combinations
Beginner:
# CMake + Make (safest)
cmake ..
make
Intermediate:
# CMake + Ninja (fast build)
cmake -G Ninja ..
ninja
Advanced:
# Meson + Ninja (modern)
meson setup build
meson compile -C build
The “beginner” line deserves one amendment: make -j$(nproc) instead of plain make, or cmake --build . --parallel, which works with any generator. Plain make on a multi-core machine leaves most of the CPU idle.
Additional Learning Resources
Official documentation:
Practical examples:
Quick Decision Guide
Simple project? → Make
Cross-platform C++? → CMake
Build speed critical? → CMake + Ninja
New project, modern syntax? → Meson + Ninja
Windows only? → Visual Studio or CMake
Large codebase? → CMake + Ninja
Frequently Asked Questions (FAQ)
Q. Do I have to write build.ninja files by hand?
A. Almost never. Ninja is designed to be a fast executor for files generated by higher-level tools, so you normally run cmake -G Ninja or let Meson generate the build files and then run ninja. Handwritten build.ninja files are mostly used for tiny projects or for learning how Ninja works.