Question

Makefile vs CMake for C and C++ Builds

cppcmakefilecmake

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.

Take Quiz

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:

  1. cmake -S . tells CMake that the source directory is the current directory, which contains CMakeLists.txt.
  2. -B build tells it to place generated files in build/, not among your source files.
  3. CMake detects available compilers and checks that they work.
  4. CMake reads add_executable and determines that calculator depends on main.cpp and add.cpp.
  5. Because the generator is Unix Makefiles, CMake writes a Makefile and supporting files into build/.
  6. cmake --build build asks the generated build tool to build that directory. In this case, it runs Make.
  7. Make compiles changed source files into object files and links them into the calculator executable.
  8. If add.cpp changes later, a subsequent build normally recompiles only add.cpp and 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, and clean targets 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 build and cmake --build build consistently, 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.
Take Quiz

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 Debug while developing and Release for 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

AspectHand-written MakefileCMake project
Main input fileMakefileCMakeLists.txt
Primary roleGives Make direct build rulesDescribes a project and generates build files
Tool that runs compilation commandsGNU MakeGenerated tool, such as Make or Ninja
Platform and IDE supportYou maintain differences yourselfCan generate for multiple supported tools and IDEs
Small project setupOften very quickSlightly more initial setup
Complex dependenciesCan become verbose and fragileTarget relationships are built into the model
Generated filesUsually none requiredBuild files are generated in a build directory
Best useSmall or environment-specific automationPortable 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.

Take Quiz

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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more