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
builddirectory - build the target using the generated backend
Slightly more realistic CMake example
cmake_minimum_required(VERSION 3.16)
project(MyApp CXX)
add_executable(app
src/main.cpp
src/math.cpp
)
target_include_directories(app PRIVATE include)
target_compile_features(app PRIVATE cxx_std_17)
This is more declarative:
- create an executable named
app - use these source files
- add the
includedirectory - require C++17 support
You do not manually write the full compile command for every file.
Key idea in the examples
The Makefile example focuses on commands. The CMake example focuses on targets and project structure.
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
cmake --build build tells CMake to invoke the generated backend.
That backend then:
- compiles
main.c - compiles
helper.c - links them into
demo
Step 5: Incremental rebuilding works
If you edit only helper.c and build again:
- only
helper.cis recompiled - then the final executable is relinked
That behavior is similar to make, because dependency-based rebuilding is still part of the generated build system.
Equivalent handwritten Makefile thinking
If you wrote the build by hand, you would need to explicitly define:
- compile rules
- dependencies
- link step
- clean step
- compiler flags
CMake automates much of that setup.
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.
Feature checks and options
Projects often expose build options:
option(ENABLE_TESTS "Build tests" ON)
This is useful for CI, packaging, and local development.
Where Makefiles still appear in real projects
Even in CMake-based projects, you may still see:
- small helper Makefiles for local developer tasks
- wrapper targets such as
make formatormake docs - legacy build systems in older codebases
But for cross-platform C/C++ applications, CMake is commonly used as the higher-level build description.
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.
5. Mixing source and build files
Beginners sometimes generate CMake build files in the source directory. That can clutter the project.
Prefer:
cmake -S . -B build
instead of running CMake directly in the source folder.
6. Treating CMake like a shell script
CMake is not just a place to dump raw compiler commands.
Less ideal style:
add_custom_target(build_all
COMMAND g++ main.cpp utils.cpp -o app
)
Better style:
add_executable(app main.cpp utils.cpp)
Use target-based commands whenever possible.
7. Assuming portability is automatic
CMake helps portability, but it does not magically fix platform-specific code. If your source code or compiler flags are platform-specific, you still need to handle that carefully.
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 | Stronger through generated project files |
| Maintenance | Can become verbose in larger projects | Often easier to scale |
Makefile vs CMake in one sentence
- Makefile: You manually tell the build tool exactly what to do.
- CMake: You describe the project, and CMake generates what the build tool should do.
Are they used together?
Yes, often.
A common workflow is:
- write
CMakeLists.txt - let CMake generate a Makefile
- build using
makeorcmake --build
Makefile vs other generated backends
One reason CMake is popular is that it can target more than Make:
- GNU Make
- Ninja
- Visual Studio
- Xcode
That flexibility is one of the biggest differences.
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
Makefile
CMakeLists.txt
build/
Common beginner confusion
CMake is not a compiler.
make is not a compiler.
Both ultimately call a compiler such as gcc or g++.
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
2D Array Loop Order and Cache Performance in C
Learn why swapping nested loops changes 2D array performance in C, using row-major memory layout, cache locality, and practical benchmarks.
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.