Question

Makefile vs CMake in C and C++: What’s the Difference?

ccppmakefilecmake

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.cpp into main.o
  • compile utils.cpp into utils.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
Take Quiz

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:

  • app depends on main.o and math.o
  • each .o file is built from its source file
  • clean removes 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 build directory
  • 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 include directory
  • 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 demo should be built from main.c and helper.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.c is 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.

Take Quiz

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 format or make 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:

  • make is 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

AspectMakefileCMake
Tool typeBuild script for makeBuild system generator
Input fileMakefileCMakeLists.txt
Main focusExact commands and dependenciesProject structure and targets
PortabilityUsually lower unless maintained carefullyHigher across tools and platforms
OutputUsed directly by makeGenerates Makefiles, Ninja files, IDE projects, etc.
Best forSmall, simple, tool-specific buildsMedium to large, cross-platform projects
IDE supportLimited by handStronger through generated project files
MaintenanceCan become verbose in larger projectsOften 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:

  1. write CMakeLists.txt
  2. let CMake generate a Makefile
  3. build using make or cmake --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.

Take Quiz

Cheat Sheet

Quick reference

  • Makefile = instructions for make
  • CMake = tool that generates build files
  • make reads a Makefile
  • 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.

Take Quiz

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

  1. Create a small C++ program with main.cpp, math_utils.cpp, and math_utils.h.
  2. Build the program with a handwritten Makefile.
  3. Build the same program with a CMakeLists.txt file.
  4. Add a clean step for the Makefile build artifacts.
  5. Use an out-of-source build directory for the CMake version.
Take Quiz

Keep learning

Related questions

View more