Question
I write C and C++ programs and currently use a GNU Makefile to compile them. CMake can also generate a Makefile that I can use to build the same project. What is the difference between compiling with a Makefile directly and using CMake to generate the build files?
Short Answer
A Makefile contains build rules that a tool such as GNU Make executes. CMake is a build-system generator: you describe your project in CMakeLists.txt, and CMake generates build files for a chosen tool, such as GNU Make, Ninja, Visual Studio, or Xcode. By the end, you will know when a hand-written Makefile is enough and when CMake makes a C/C++ project easier to maintain and share.
Concept
A C/C++ build usually involves more than running one compiler command. Source files must be compiled, object files must be linked, headers affect rebuild decisions, compiler flags differ across platforms, and external libraries may need to be found.
A Makefile is a file containing rules for make. Each rule says how to create a target from its prerequisites:
app: main.o math.o
g++ main.o math.o -o app
GNU Make reads this rule and decides whether app needs rebuilding.
CMake does not normally compile your source code itself. Instead, it reads a higher-level project description, usually named CMakeLists.txt, then generates files for another build tool. For example:
CMakeLists.txt -> CMake -> Makefile -> make -> compiler/linker
CMake can generate a Makefile, but it can also generate Ninja build files or IDE project files. This separation matters because your project description can remain mostly the same while the generated build system changes for each developer, operating system, or compiler.
A useful distinction is:
- Make: executes build rules.
- Makefile: the build instructions written for Make.
- CMake: configures a project and generates build instructions for a selected build tool.
- CMakeLists.txt: the project description read by CMake.
CMake is not inherently a replacement for every Makefile. A small project with a few files can be perfectly well served by a simple hand-written Makefile. CMake becomes especially useful as a project needs portability, dependencies, tests, install rules, libraries, multiple build configurations, or IDE support.
Mental Model
Think of a Makefile as a set of detailed assembly instructions written specifically for one workshop: “Use this machine, in this order, with these settings.” GNU Make follows those instructions.
Think of CMake as a planner that receives a description of what you want to build: “This project produces an application from these files and needs this library.” The planner then creates instructions suitable for the workshop available:
- GNU Make instructions on one machine
- Ninja instructions on another
- Visual Studio project files on Windows
- Xcode project files on macOS
When CMake generates a Makefile, Make still performs the actual incremental build. CMake prepared the Makefile for it.
Syntax and Examples
A small hand-written GNU Makefile might look like this:
CXX := g++
CXXFLAGS := -Wall -Wextra -std=c++17
app: main.o greeting.o
$(CXX) main.o greeting.o -o app
main.o: main.cpp greeting.hpp
$(CXX) $(CXXFLAGS) -c main.cpp -o main.o
greeting.o: greeting.cpp greeting.hpp
$(CXX) $(CXXFLAGS) -c greeting.cpp -o greeting.o
clean:
rm -f app *.o
Build it with:
make
This file explicitly describes compilation and linking commands. The tab before each command is required by traditional Makefile syntax.
The equivalent basic CMake project can be described in CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(GreetingApp LANGUAGES CXX)
add_executable(app
main.cpp
greeting.cpp
)
target_compile_features(app PRIVATE cxx_std_17)
target_compile_options(app PRIVATE -Wall -Wextra)
Configure and build it in a separate build directory:
cmake -S . -B build
cmake --build build
On a system where CMake selected the Unix Makefiles generator, the first command creates build/Makefile. The second command invokes the generated build system. You can request a generator explicitly:
cmake -S . -B build -G Ninja
cmake --build build
The same CMakeLists.txt can now create Ninja files instead of a Makefile.
Step by Step Execution
Consider this CMake project:
cmake_minimum_required(VERSION 3.16)
project(Calculator LANGUAGES CXX)
add_executable(calculator main.cpp add.cpp)
target_compile_features(calculator PRIVATE cxx_std_17)
Run:
cmake -S . -B build -G "Unix Makefiles"
cmake --build build
What happens:
cmake -S .tells CMake that the source directory is the current directory, which containsCMakeLists.txt.-B buildtells it to place generated files inbuild/, not among your source files.- CMake detects available compilers and checks that they work.
- CMake reads
add_executableand determines thatcalculatordepends onmain.cppandadd.cpp. - Because the generator is
Unix Makefiles, CMake writes a Makefile and supporting files intobuild/. cmake --build buildasks the generated build tool to build that directory. In this case, it runs Make.- Make compiles changed source files into object files and links them into the
calculatorexecutable. - If
add.cppchanges later, a subsequent build normally recompiles onlyadd.cppand relinks the executable.
With a hand-written Makefile, you write steps 4 and 5 yourself as Make rules. With CMake, you describe the target and let CMake generate those lower-level rules.
Real World Use Cases
- Small command-line tool: A short Makefile can provide simple
build,run,test, andcleantargets with very little setup. - Cross-platform C++ library: CMake can generate build files for Linux, macOS, and Windows users from one project description.
- CI pipelines: A project can use
cmake -S . -B buildandcmake --build buildconsistently, while the CI environment chooses Ninja, Make, or a platform-specific generator. - IDE integration: Many C++ IDEs understand CMake projects and can configure, build, test, and debug their targets.
- External dependencies: CMake can locate installed packages, define imported targets, and express that one target links to another.
- Release packaging: CMake can describe install rules so a library, executable, and public headers are installed in predictable locations.
Real Codebase Usage
In production CMake projects, developers usually model the project as targets rather than manually constructing compiler commands.
For example, a library and executable can be expressed like this:
add_library(greeting greeting.cpp)
target_include_directories(greeting PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_features(greeting PUBLIC cxx_std_17)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE greeting)
This is preferable to copying include paths and library flags onto every compile command. CMake propagates appropriate usage requirements to targets that link greeting.
Common real-project practices include:
- Out-of-source builds: Generate all temporary files under
build/so source directories stay clean. - Named targets: Build a specific output with
cmake --build build --target app. - Build configurations: Use
Debugwhile developing andReleasefor optimized binaries. The exact command depends on whether the selected generator supports multiple configurations. - Testing: CMake projects often enable tests and run them with CTest.
- Installation:
install(TARGETS ...)rules support packaging and deployment. - Tool choice: CMake may generate Ninja files for fast command-line builds even when Make is installed. Developers generally do not need to edit generated Makefiles directly.
Makefiles also remain common in real repositories, especially as a convenient command interface:
build:
cmake -S . -B build
cmake --build build
test:
ctest --test-dir build --output-on-failure
Here, Make provides memorable commands, while CMake manages the C++ build configuration.
Common Mistakes
Editing a CMake-generated Makefile
Generated files are outputs, not the source of truth:
# Do not manually edit build/Makefile generated by CMake.
Your changes can disappear the next time CMake configures the project. Change CMakeLists.txt instead.
Treating CMake as the compiler
CMake configures and generates build files; the compiler still does compilation. This command configures a project but does not necessarily build it:
cmake -S . -B build
Build afterwards:
cmake --build build
Building inside the source directory
Avoid placing generated files next to .cpp and .hpp files:
# Less desirable for most projects
cmake .
Prefer:
cmake -S . -B build
Forgetting header dependencies in a hand-written Makefile
This incomplete rule may fail to rebuild main.o after config.hpp changes:
main.o: main.cpp
g++ -c main.cpp -o main.o
List relevant headers or use compiler-generated dependency files. CMake-generated Makefiles commonly arrange dependency tracking for supported compilers.
Putting global flags everywhere in CMake
Avoid relying on global settings when target-specific settings communicate intent better:
# Often avoid for project-specific options
add_compile_options(-Wall)
Prefer:
target_compile_options(app PRIVATE -Wall)
Target-based commands prevent unrelated targets from accidentally inheriting settings.
Assuming every compiler uses the same flags
-Wall is widely used with GCC and Clang, but Microsoft Visual C++ uses different warning options. Cross-platform projects may need compiler-specific conditions or carefully chosen portable settings.
Comparisons
| Aspect | Hand-written Makefile | CMake project |
|---|---|---|
| Main input file | Makefile | CMakeLists.txt |
| Primary role | Gives Make direct build rules | Describes a project and generates build files |
| Tool that runs compilation commands | GNU Make | Generated tool, such as Make or Ninja |
| Platform and IDE support | You maintain differences yourself | Can generate for multiple supported tools and IDEs |
| Small project setup | Often very quick | Slightly more initial setup |
| Complex dependencies | Can become verbose and fragile | Target relationships are built into the model |
| Generated files | Usually none required | Build files are generated in a build directory |
| Best use | Small or environment-specific automation | Portable or growing C/C++ projects |
CMake versus GNU Make
They are not direct alternatives at the same layer. CMake can choose GNU Make as its generated backend. In that setup, CMake and Make work together.
CMake versus Ninja
Ninja, like Make, is a build tool that executes build rules. CMake can generate Ninja build files. Many developers choose Ninja because it is designed to be fast and has a simple build-file format, while CMake remains the project configuration layer.
Hand-written Makefile versus CMake-generated Makefile
A hand-written Makefile is maintained by you. A CMake-generated Makefile is maintained by CMake based on CMakeLists.txt. Both may ultimately invoke the same compiler with similar commands.
Cheat Sheet
-
Makefile: Rules consumed by
make. -
CMake: A build-system generator; it usually does not compile code directly.
-
CMakeLists.txt: The CMake project configuration file.
-
Typical CMake flow:
cmake -S . -B build cmake --build build -
Choose a generator:
cmake -S . -B build -G Ninja -
Core CMake commands:
add_executable(app main.cpp) add_library(core core.cpp) target_link_libraries(app PRIVATE core) target_compile_features(app PRIVATE cxx_std_17) -
Keep generated files in
build/. -
Do not edit generated Makefiles; edit
CMakeLists.txt. -
A Makefile can still be useful as a shortcut layer for commands such as
make test. -
Use a simple hand-written Makefile for small, stable builds; consider CMake when portability, dependencies, IDEs, tests, or installation matter.
FAQ
Is CMake better than Make?
Not in every situation. CMake is often more suitable for portable or multi-target C/C++ projects. A small Makefile may be simpler for a small project with one environment.
Does CMake compile C++ code?
Usually no. CMake configures the project and generates build files. A backend tool such as Make or Ninja invokes the compiler and linker.
Why does CMake generate a Makefile?
Make is one supported backend. CMake generates Make rules from the target definitions in CMakeLists.txt, allowing Make to perform the build.
Can I use CMake and Make together?
Yes. CMake can generate a Makefile, and Make then builds the project. A separate hand-written Makefile can also call CMake commands as shortcuts.
Should I commit the generated Makefile from CMake?
Usually no. Generated build files are normally recreated locally in a build directory. Commit the CMakeLists.txt and source files instead, unless a project has an explicit reason to version generated files.
What should I run after changing CMakeLists.txt?
Run CMake configuration again, then build:
cmake -S . -B build
cmake --build build
Can CMake create Visual Studio projects or Ninja files instead of Makefiles?
Yes. CMake supports multiple generators, including Ninja and generators for several IDEs and platforms.
Do I need CMake for every C++ project?
No. You can compile directly, use Make, or use another build system. CMake is a common choice when its generator and project-management features solve real project needs.
Mini Project
Description
Create a small C++ greeting application described with CMake. The project has a reusable library target and an executable target, demonstrating how CMake expresses relationships between parts of a program without writing compiler commands manually.
Goal
Configure and build a C++ application that links an executable to a small local library using CMake.
Requirements
Create main.cpp, greeting.cpp, greeting.hpp, and CMakeLists.txt.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.