Question
I write C and C++ programs and currently use a GNU Makefile to compile them. I understand that I can also use CMake and have it generate a Makefile for me. What is the difference between writing and using a Makefile directly versus using CMake to build and compile the code?
Short Answer
By the end of this page, you will understand the difference between Makefiles and CMake in C and C++ projects. You will learn what each tool does, how they relate to each other, when to use one over the other, and why many real projects use CMake to generate build files instead of maintaining raw Makefiles by hand.
Concept
A Makefile and CMake are related, but they are not the same kind of tool.
- A Makefile is a build script used by
make. - CMake is a build system generator.
That distinction is the key idea.
What a Makefile is
A Makefile tells the make program:
- what files need to be built
- what commands should run to build them
- which files depend on which other files
For example, a Makefile can say:
- compile
main.cppintomain.o - compile
utils.cppintoutils.o - link both object files into
app
make then uses those rules to decide what needs rebuilding.
What CMake is
CMake does not usually compile your code directly by itself. Instead, you write a CMakeLists.txt file describing your project at a higher level, such as:
- this project uses C++
- these source files belong to this executable
- this target depends on this library
- use this include directory
Then CMake generates actual build files for a specific build tool, such as:
- Unix Makefiles
- Ninja files
- Visual Studio project files
- Xcode project files
So if CMake generates a Makefile, the real build still happens through make, but the Makefile was produced automatically.
Why this matters
This difference matters because Makefiles are low-level and tool-specific, while CMake is higher-level and more portable.
With a handwritten Makefile, you are usually describing:
- exact compiler commands
- exact flags
- exact file paths
- exact build steps for one toolchain or environment
With CMake, you usually describe:
- targets
- source files
- libraries
- features
- dependencies
CMake then translates that into build instructions suitable for the current platform and generator.
In simple terms
- Makefile: "Run these exact commands in this dependency order."
- CMake: "This is my project structure; generate the right build files for this environment."
Why developers often prefer CMake for larger projects
CMake helps when a project needs to:
- build on Linux, macOS, and Windows
- support multiple compilers
- manage libraries and dependencies
- generate IDE project files
- keep build logic more maintainable as the codebase grows
A simple Makefile can be perfect for a small project, but as complexity increases, maintaining it manually often becomes harder.
Important clarification
CMake is not just a "different way to write Makefiles." It is a separate layer above build tools. One of its outputs can be a Makefile, but it can also generate other formats.
Mental Model
Think of it like writing directions for a trip versus using a route planner.
- A Makefile is like writing every turn yourself:
- go straight 2 miles
- turn left
- merge onto highway
- CMake is like telling a route planner your destination and preferences:
- start here
- end there
- avoid tolls
- use the best route for this region
The route planner then creates the detailed turn-by-turn instructions.
In the same way:
- a Makefile contains concrete build commands
- CMake describes the project and generates those commands for the chosen build tool
Another analogy:
- Makefile = recipe with exact kitchen steps
- CMake = meal planner that creates the recipe based on your kitchen and tools
Syntax and Examples
Basic handwritten Makefile
CC = gcc
CFLAGS = -Wall -Wextra -O2
app: main.o math.o
$(CC) $(CFLAGS) -o app main.o math.o
main.o: main.c math.h
$(CC) $(CFLAGS) -c main.c
math.o: math.c math.h
$(CC) $(CFLAGS) -c math.c
clean:
rm -f app *.o
This Makefile says:
appdepends onmain.oandmath.o- each
.ofile is built from its source file cleanremoves generated files
You are responsible for writing the commands and dependencies yourself.
Basic CMake file
cmake_minimum_required(VERSION 3.16)
project(MyApp C)
add_executable(app main.c math.c)
Then you typically run:
cmake -S . -B build
cmake --build build
This tells CMake to:
- read
CMakeLists.txt - generate build files in the directory
Step by Step Execution
Consider this simple CMake file:
cmake_minimum_required(VERSION 3.16)
project(Demo C)
add_executable(demo main.c helper.c)
And these commands:
cmake -S . -B build
cmake --build build
What happens step by step
Step 1: CMake reads CMakeLists.txt
CMake sees:
- minimum version is 3.16
- project name is
Demo - language is C
- executable
demoshould be built frommain.candhelper.c
Step 2: CMake checks your environment
It detects things like:
- available compiler
- compiler type
- platform
- selected generator
For example, on Linux it may generate Unix Makefiles. On another system it may generate Ninja files or a Visual Studio project.
Step 3: CMake generates build files
Inside build/, CMake writes the files needed by the chosen backend.
If the generator is Makefiles, it creates generated Makefiles there.
Step 4: The actual build runs
tells CMake to invoke the generated backend.
Real World Use Cases
When a handwritten Makefile is a good fit
A plain Makefile is often enough when:
- the project is very small
- you only build on one platform
- you use one compiler setup
- you want full manual control over commands
- the build process is simple and stable
Examples:
- a small C utility program
- a classroom assignment
- an embedded experiment with a fixed toolchain
- a quick local automation task
When CMake is a better fit
CMake becomes useful when:
- the project must work on different operating systems
- developers use different compilers or IDEs
- the codebase has multiple libraries and executables
- external dependencies need to be found and linked
- CI pipelines need a reproducible build setup
Examples:
- a cross-platform C++ desktop application
- a shared library used by many teams
- an open-source C++ project distributed to many users
- a game engine, SDK, or command-line tool with optional features
In real teams
Many teams choose CMake because it helps standardize builds across environments. Instead of maintaining:
- one Makefile for Linux
- one Visual Studio project for Windows
- one Xcode project for macOS
They maintain one CMake configuration and generate the platform-specific files as needed.
Real Codebase Usage
In real codebases, developers usually treat CMake as the source of truth for the build, not the generated Makefiles.
Common patterns in real projects
Target-based builds
Modern CMake encourages working with targets:
add_library(core src/core.cpp)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
This is cleaner than manually repeating include paths and compiler flags everywhere.
Configuration by build type
Projects often support:
- Debug
- Release
- RelWithDebInfo
CMake makes it easier to express these configurations in a portable way.
Dependency management
Real projects often need third-party libraries.
CMake commonly uses patterns such as:
find_package(...)target_link_libraries(...)- imported targets from installed libraries
Example:
find_package(ZLIB REQUIRED)
target_link_libraries(app PRIVATE ZLIB::ZLIB)
Separate source and build directories
Developers usually build out of source:
cmake -S . -B build
cmake --build build
This keeps generated files out of the source tree.
Common Mistakes
1. Thinking CMake and Make are competitors at the same level
A common misunderstanding is:
- "Should I use CMake instead of make?"
More accurately:
makeis a build tool- CMake is a generator and configuration tool
CMake may generate files that make uses.
2. Editing generated Makefiles by hand
If CMake generates a Makefile, beginners sometimes edit it directly.
That is usually a mistake because the file may be regenerated and overwrite your changes.
Better approach: edit CMakeLists.txt, then rerun CMake.
3. Using CMake for tiny projects without understanding the basics
CMake is useful, but for a two-file program it may feel like overkill if you do not yet understand compiling and linking.
It is still valuable to learn:
- source files
- object files
- linking
- dependencies
A simple Makefile teaches these ideas clearly.
4. Writing fragile handwritten Makefiles
Broken example:
app:
gcc -o app main.c math.c
This works for a tiny case, but it does not express per-file dependencies well and can become hard to maintain.
A better Makefile tracks object files and headers.
Comparisons
| Aspect | Makefile | CMake |
|---|---|---|
| Tool type | Build script for make | Build system generator |
| Input file | Makefile | CMakeLists.txt |
| Main focus | Exact commands and dependencies | Project structure and targets |
| Portability | Usually lower unless maintained carefully | Higher across tools and platforms |
| Output | Used directly by make | Generates Makefiles, Ninja files, IDE projects, etc. |
| Best for | Small, simple, tool-specific builds | Medium to large, cross-platform projects |
| IDE support | Limited by hand |
Cheat Sheet
Quick reference
- Makefile = instructions for
make - CMake = tool that generates build files
makereads aMakefile- CMake reads
CMakeLists.txt - CMake can generate:
- Makefiles
- Ninja files
- Visual Studio solutions
- Xcode projects
Typical Make workflow
make
make clean
Typical CMake workflow
cmake -S . -B build
cmake --build build
Use a Makefile when
- the project is small
- the toolchain is fixed
- you want direct low-level control
Use CMake when
- the project is cross-platform
- you need IDE support
- the project has multiple targets or dependencies
- the codebase is expected to grow
Key rule
Do not edit generated Makefiles from CMake.
Edit CMakeLists.txt instead.
Mental shortcut
- Makefile = build commands
- CMake = build description
Common files
FAQ
Is CMake just a nicer Makefile?
Not exactly. CMake is a higher-level build configuration tool that can generate a Makefile, but it can also generate other build formats.
Does CMake compile code by itself?
Usually no. CMake configures and generates build files, then a backend tool such as make, ninja, or Visual Studio performs the actual build steps.
Should I learn Makefiles before CMake?
It helps. Understanding Makefiles teaches core ideas such as compilation, linking, dependencies, and incremental builds.
Why do many C++ projects use CMake?
Because it is useful for cross-platform builds, multiple compilers, IDE integration, and dependency management.
Is a Makefile faster than CMake?
They are not directly comparable in that way. CMake generates the build system; the actual build speed often depends more on the backend tool and project structure.
Can I use both CMake and Make together?
Yes. A very common setup is to use CMake to generate a Makefile, then use that generated Makefile to build.
When is a handwritten Makefile enough?
When the project is small, local, and simple, and you do not need cross-platform generation or IDE integration.
Should I edit the Makefile generated by CMake?
No. Your changes will likely be overwritten. Change CMakeLists.txt and regenerate instead.
Mini Project
Description
Create a small C++ project in two ways: first with a handwritten Makefile, then with CMake. This helps you see that both approaches can build the same program, but they express the build differently. The project demonstrates source files, headers, compilation, linking, and a clean build workflow.
Goal
Build the same small C++ program using both a Makefile and CMake, and compare how each approach describes the build.
Requirements
- Create a small C++ program with
main.cpp,math_utils.cpp, andmath_utils.h. - Build the program with a handwritten Makefile.
- Build the same program with a
CMakeLists.txtfile. - Add a clean step for the Makefile build artifacts.
- Use an out-of-source build directory for the CMake version.
Keep learning
Related questions
Array-to-Pointer Conversion in C and C++ Explained
Learn what array-to-pointer conversion means in C and C++, how array decay works, and how it differs from a pointer to an array.
Building More Fault-Tolerant Embedded C++ Applications for Radiation-Prone ARM Systems
Learn practical C++ and compile-time techniques to reduce soft-error damage in embedded ARM systems exposed to radiation.
C Pointer to Array vs Array of Pointers: How to Read Complex Declarations
Learn the difference between pointer-to-array and array-of-pointers in C, plus a simple rule for reading complex declarations correctly.