1. From Makefile to CMake: Why Learn Yet Another Tool?
In the previous articles we’ve been talking about the Makefile the whole time. Is it good enough?
If your project is only ever compiled with gcc on Linux, make is basically enough. But as soon as you need to:
- Compile with Visual Studio on Windows
- Compile with Xcode on macOS
- Detect whether a certain library exists on the system
- Choose different compilation options according to the system version
- Get every team member (each running a different operating system) to build successfully
Hand-writing a Makefile turns into a nightmare. The compilers and build tools on different platforms are two entirely different languages — Windows uses cl.exe and nmake, Linux uses gcc and make, and there is no way to write one Makefile that works everywhere.
CMake exists to solve exactly this: you write one build description, and it generates the build files for the corresponding platform.
2. Where CMake Fits
CMake is not the build system itself — it is a build system generator.
CMakeLists.txt → cmake → generates build files → the build system performs compilation
Let me use an analogy:
makeis the chef, stir-frying the dish directly- CMake is the ordering system — you tell it which dishes you want, and it generates the menu that suits this particular kitchen (Makefile / Ninja / Visual Studio project files)
The biggest benefit of this design: one CMakeLists.txt, usable everywhere. Whether a Linux developer is using make or a Windows developer is using Visual Studio, everyone edits the same CMakeLists.txt.
3. Your First CMakeLists.txt
Suppose you have a single-file C project:
myapp/
├── main.c
└── CMakeLists.txt
CMakeLists.txt can be as simple as three lines:
cmake_minimum_required(VERSION 3.10)
project(MyApp C)
add_executable(myapp main.c)
A quick explanation:
cmake_minimum_required: declares the minimum CMake version you need. This is the first line of every CMakeLists.txt.project: gives your project a name and declares the languages used (C, CXX and so on).add_executable: tells CMake we want to produce an executable calledmyappwhose source ismain.c.
Looks simple enough? That’s because CMake has a great deal of default behaviour — it detects your compiler and system type automatically and configures everything for you.
4. The Build Workflow
CMake has one very important habit: out-of-source builds.
What that means: don’t let the temporary files produced by compilation pollute the clean source directory.
The standard workflow:
cd myapp
mkdir build && cd build
cmake ..
make
mkdir build: create the build directorycmake ..: tell cmake to look forCMakeLists.txtin the parent directory and generate the build files in the current directorymake: run the compilation
All the build output lives in build/, and the source directory stays spotless. Want to clean up? Just rm -rf build. That’s the benefit of out-of-source builds.
5. Specifying the Compiler and Build Type
CMake lets you control compilation behaviour flexibly:
# Specify the C compiler
cmake .. -DCMAKE_C_COMPILER=/usr/bin/clang
# Specify the build type: Debug (with debug info) / Release (optimized) / RelWithDebInfo (optimized + debug info)
cmake .. -DCMAKE_BUILD_TYPE=Debug
The -D argument is how CMake is configured; the format is -D变量名=值. You can override any variable from CMakeLists.txt on the command line, which is very flexible.
6. Common Variables and Options
A slightly more substantial CMakeLists.txt:
cmake_minimum_required(VERSION 3.10)
project(MyApp C)
# Set the C standard
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# Add compilation options
add_compile_options(-Wall -Wextra)
# Add preprocessor definitions
add_definitions(-DVERSION=\"1.0\")
# Executable
add_executable(myapp main.c utils.c network.c)
set(): sets a variableadd_compile_options(): adds compilation options globallyadd_definitions(): adds macro definitions (equivalent to-D)
7. How Does CMake Find the Makefile?
You may be wondering: I ran cmake .., how does it know to generate a Makefile rather than Visual Studio project files?
CMake has a concept called a generator. By default it chooses one automatically based on your operating system:
- Linux / macOS:
Unix Makefiles - Windows: the generator for the corresponding Visual Studio version
You can also specify it manually:
# Generate Ninja build files (faster)
cmake .. -G Ninja
ninja
# Generate an Xcode project (macOS)
cmake .. -G Xcode
For now you don’t need to agonize over this; the default make is perfectly good.
8. CMake vs Makefile: A Comparison Table
| Need | Makefile | CMake |
|---|---|---|
| Add a source file | Manually edit the OBJS variable | add_executable(app main.c foo.c) |
| Specify compilation options | CFLAGS += -Wall |
add_compile_options(-Wall) |
| Bring in a library | Write -I -L -l by hand |
find_package() + target_link_libraries() |
| Cross-platform | Basically impossible | Supported natively |
| Learning curve | Low | Medium |
CMake is indeed a bit steeper to learn than Makefile, but once you get the hang of it, the maintenance cost of large projects drops a great deal.
In the next article we’ll learn the right way to approach “modern CMake” — the target_* family. This may be the most important lesson in your whole CMake journey.
A small step forward every day is a new height!