LEARN CMAKE MASTERY
Learn CMake: From Beginner to Build System Master
Goal: To achieve a deep, practical understanding of modern CMake for building C and C++ projects of any scale—from a single file to a complex, multi-platform ecosystem with libraries, dependencies, testing, and packaging.
Why Learn CMake?
In the C++ world, compiling code is easy, but building systems is hard. CMake is the de facto industry standard for managing this complexity. It’s a build system generator that automates the process of compiling, linking, testing, and packaging software across different platforms and compilers. Mastering CMake is not just a useful skill; it’s essential for any serious C++ developer.
After completing these projects, you will:
- Understand and write clean, modern, and maintainable
CMakeLists.txtfiles. - Structure complex projects with multiple libraries and executables.
- Find and integrate third-party dependencies seamlessly.
- Manage platform-specific code and build configurations.
- Create a full testing and installation pipeline for your applications.
Core Concept Analysis
The CMake Workflow
┌─────────────────────────────┐ ┌──────────────────────────┐ ┌────────────────────────┐
│ CMakeLists.txt │ │ Makefile / │ │ │
│ (Your project's blueprint) ├─────►│ .vscode/ c_cpp_properties.json ├─────►│ Executable / Library │
│ │ │ Visual Studio .sln │ │ (Your final program) │
└─────────────────────────────┘ └──────────────────────────┘ └────────────────────────┘
(1. Configure) (2. Generate) (3. Build)
`cmake -B build` (CMake creates native `cmake --build build`
build files)
Key Concepts Explained (“Modern CMake”)
Modern CMake (version 3.0+) revolves around the concept of targets and properties. Instead of telling CMake how to do something (e.g., “add this global flag”), you define what you are building (targets) and describe their requirements (properties).
- Targets: Anything you build.
add_executable(...): Creates an executable target.add_library(...): Creates a library target (static or shared).
- Properties: The attributes of a target. You modify these with
target_*commands.target_include_directories(...): Specifies header search paths for a target.target_link_libraries(...): Links a target against other libraries.target_compile_definitions(...): Adds preprocessor definitions for a target.target_compile_options(...): Adds compiler flags for a target.
- Visibility Keywords (
PRIVATE,INTERFACE,PUBLIC): These control how properties propagate between targets.PRIVATE: The property is only applied to the current target.INTERFACE: The property is only applied to targets that link against this one.PUBLIC: The property is applied to both the current target and targets that link against it (most common for include directories and definitions).
- Dependencies:
find_package(...): Finds external libraries (like Boost, Qt, SDL) that are already installed on the system.FetchContent: A modern way to download and build a dependency at configure time, making your project self-contained.
Project List
You will build a single C++ command-line greeting application, but you will evolve its build system through these 10 projects, starting from a single file and ending with a fully packaged, cross-platform application.
Project 1: The Simplest Executable
- File: LEARN_CMAKE_MASTERY.md
- Main Programming Language: C++
- Alternative Programming Languages: C
- Coolness Level: Level 1: Pure Corporate Snoozefest
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Build Systems / C++
- Software or Tool: CMake, a C++ compiler (GCC, Clang, MSVC)
- Main Book: “Professional CMake: A Practical Guide” by Craig Scott
What you’ll build: A CMakeLists.txt file to compile a single “Hello, World!” C++ source file into an executable.
Why it teaches CMake: This is the absolute minimum required to get a project to build. It teaches the fundamental structure of a CMakeLists.txt file and the out-of-source build workflow, which is crucial for keeping your source directory clean.
Core challenges you’ll face:
- Writing the
CMakeLists.txtfile → maps to usingcmake_minimum_required,project, andadd_executable - Running CMake for the first time → maps to the
cmake -B buildandcmake --build buildcommands - Understanding the build directory → maps to why out-of-source builds are the standard
Key Concepts:
- CMake Basics: Official CMake Tutorial - Step 1
- Out-of-Source Builds: “Professional CMake” Ch. 1 - Scott
Difficulty: Beginner Time estimate: Less than an hour Prerequisites: A C++ compiler installed.
Real world outcome:
A clean source directory and a build directory containing all the intermediate build files and the final executable.
$ tree .
.
├── CMakeLists.txt
└── main.cpp
$ cmake -B build
-- The CXX compiler identification is AppleClang 14.0.0.14000029
-- Configuring done
-- Generating done
-- Build files have been written to: /path/to/project/build
$ cmake --build build
[100%] Built target Greeter
$ ./build/Greeter
Hello, CMake!
Implementation Hints:
Your main.cpp can be a simple std::cout << "Hello, CMake!" << std::endl;.
Your CMakeLists.txt needs only three commands:
cmake_minimum_required(VERSION 3.10)project(Greeter)add_executable(Greeter main.cpp)
Learning milestones:
- CMake generates build files without errors → Your
CMakeLists.txtsyntax is correct. - The project compiles successfully → CMake correctly found your compiler.
- You can run the executable from the build directory → You understand the build process.
Project 2: Creating and Linking a Library
- File: LEARN_CMAKE_MASTERY.md
- Main Programming Language: C++
- Alternative Programming Languages: C
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 1: Beginner
- Knowledge Area: Build Systems / C++
- Software or Tool: CMake
- Main Book: “Professional CMake: A Practical Guide” by Craig Scott
What you’ll build: You will refactor the “Hello” logic into its own static library. The main executable will link against this library to print the message.
Why it teaches CMake: This introduces the core concept of targets. You learn that a project is a graph of dependencies between targets (executables and libraries). This is the foundation of “Modern CMake”.
Core challenges you’ll face:
- Creating a library target → maps to using the
add_librarycommand - Linking a library to an executable → maps to using
target_link_libraries - Managing source files for different targets → maps to associating specific source files with each target
Key Concepts:
- Targets: “Professional CMake” Ch. 2 - Scott
- Linking Libraries: Official CMake Tutorial - Step 2
Difficulty: Beginner Time estimate: 1-2 hours Prerequisites: Project 1.
Real world outcome: The same “Hello, CMake!” output as before, but your project is now better structured, separating implementation (the library) from the entry point (the executable).
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(Greeter)
# Create a library for the greeting logic
add_library(Greeting STATIC
greeting.cpp
greeting.h
)
# Create the executable
add_executable(Greeter main.cpp)
# Link the executable against the library
target_link_libraries(Greeter PRIVATE Greeting)
Implementation Hints:
- Create
greeting.hwith a function declarationvoid print_greeting();. - Create
greeting.cppwith the implementation ofprint_greeting(). - Modify
main.cppto#include "greeting.h"and callprint_greeting(). - Update your
CMakeLists.txtto define the two targets and the link dependency between them.
Learning milestones:
- Both the library and executable compile → You have defined your targets correctly.
- The program runs and prints the message → You have successfully linked the targets.
- You understand the difference between
add_libraryandadd_executable→ You are thinking in terms of targets.
Project 3: Managing Directories and Headers
- File: LEARN_CMAKE_MASTERY.md
- Main Programming Language: C++
- Alternative Programming Languages: C
- Coolness Level: Level 2: Practical but Forgettable
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Build Systems / Project Structure
- Software or Tool: CMake
- Main Book: “Mastering CMake” by Kitware, Inc.
What you’ll build: You will move the Greeting library into its own subdirectory (src/greeting) with its own CMakeLists.txt. The top-level CMakeLists.txt will use add_subdirectory to include it.
Why it teaches CMake: This teaches you how to structure a large project. By breaking the project into modules with their own build scripts, you create a scalable and maintainable build system. It also forces you to understand how to expose header files from a library to its consumers.
Core challenges you’ll face:
- Creating a nested
CMakeLists.txt→ maps to making your build system modular - Using
add_subdirectory→ maps to composing a large project from smaller parts - Exposing header files → maps to using
target_include_directorieswith thePUBLICkeyword - Understanding target scope → maps to how targets defined in subdirectories are visible to the parent
Key Concepts:
- Subdirectories: Official CMake Tutorial - Step 3
- Include Directories and Usage Requirements: “Effective Modern CMake” - a blog post by Stephen Kelly
Difficulty: Intermediate Time estimate: A weekend Prerequisites: Project 2.
Real world outcome: Your project directory is now much more organized. The build process works exactly as before, but the structure can now scale to hundreds of libraries and executables.
.
├── CMakeLists.txt
├── main.cpp
└── src
└── greeting
├── CMakeLists.txt
├── greeting.cpp
└── greeting.h
Implementation Hints:
- Create the directory structure shown above.
- In
src/greeting/CMakeLists.txt, define theGreetinglibrary target. - In that same file, use
target_include_directories(Greeting PUBLIC .)to specify that consumers ofGreetingneed to have the current directory (src/greeting) in their include path to findgreeting.h. - In the top-level
CMakeLists.txt, remove theadd_librarycommand. - In its place, use
add_subdirectory(src/greeting). - The
add_executableandtarget_link_librariescommands in the top-level file remain the same! CMake automatically makes theGreetingtarget available.
Learning milestones:
- The project builds from the top-level
CMakeLists.txt→add_subdirectoryis working. main.cppcan still findgreeting.hwithout..in the path →target_include_directoriesis correctly propagating usage requirements.- The project can be built from the subdirectory as well → Your modules are self-contained.
Project 4: Adding an External Dependency
- File: LEARN_CMAKE_MASTERY.md
- Main Programming Language: C++
- Alternative Programming Languages: C
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 1. The “Resume Gold”
- Difficulty: Level 2: Intermediate
- Knowledge Area: Build Systems / Dependency Management
- Software or Tool: CMake, Git
- Main Book: “Professional CMake: A Practical Guide” by Craig Scott
What you’ll build: You will modify the Greeting library to use a third-party library, {fmt}, for string formatting. You will use CMake’s FetchContent module to download and build {fmt} as part of your project’s configuration step.
Why it teaches CMake: Real-world projects are built on the shoulders of giants. Knowing how to find and integrate external libraries is a critical skill. FetchContent provides a modern, robust way to manage dependencies, making your project self-contained and easy to build for others.
Core challenges you’ll face:
- Finding a dependency → maps to the
find_packagecommand and its modes (Config vs. Module) - Fetching content at configure time → maps to using
FetchContentfor a more reproducible build - Linking against a third-party target → maps to using the
fmt::fmttarget that the{fmt}library exports
Key Concepts:
FetchContent: Official CMake documentationfind_package: “Professional CMake” Ch. 8 - Scott- Exported Targets: Understanding that well-behaved CMake projects provide targets for you to link against.
Difficulty: Intermediate Time estimate: 2-3 hours Prerequisites: Project 3.
Real world outcome:
Your greeter application now uses the powerful {fmt} library to produce its output. Anyone can clone your repository and build the project with a single cmake command, without needing to install {fmt} manually.
// greeting.cpp
#include <fmt/core.h>
void print_greeting() {
fmt::print("Hello, {}!\n", "CMake from {fmt}");
}
# Top-level CMakeLists.txt
...
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0 # Use a specific tag for reproducibility
)
FetchContent_MakeAvailable(fmt)
...
# src/greeting/CMakeLists.txt
...
# Link our library against {fmt}
target_link_libraries(Greeting PUBLIC fmt::fmt)
Implementation Hints:
- In your top-level
CMakeLists.txt, include theFetchContentmodule. - Declare the
{fmt}dependency usingFetchContent_Declare, providing its Git repository and a specific version tag. - Call
FetchContent_MakeAvailable(fmt). This will download{fmt}and run itsCMakeLists.txt, making its targets (likefmt::fmt) available to your project. - In
src/greeting/CMakeLists.txt, addfmt::fmtto yourtarget_link_librariescall. ThePUBLICkeyword is important here, as it ensures that if another target links againstGreeting, it also correctly links against{fmt}. - Update your
greeting.cppto use the{fmt}library.
Learning milestones:
- CMake configures and downloads the
{fmt}source code →FetchContentis working. - Your
Greetinglibrary compiles and links against{fmt}→ You are correctly using the imported target. - The final executable runs and shows the formatted message → The entire dependency chain is correct.
Project 5: Adding Tests with CTest
- File: LEARN_CMAKE_MASTERY.md
- Main Programming Language: C++
- Alternative Programming Languages: C
- Coolness Level: Level 3: Genuinely Clever
- Business Potential: 3. The “Service & Support” Model
- Difficulty: Level 3: Advanced
- Knowledge Area: Build Systems / Software Testing
- Software or Tool: CMake, CTest, GoogleTest
- Main Book: “Mastering CMake” by Kitware, Inc.
What you’ll build: A unit test for your Greeting library using the GoogleTest framework. You will use CMake to fetch GoogleTest, build a test executable, and define a test that can be run with ctest.
Why it teaches CMake: Building your code is only half the battle; you also need to test it. CMake has a built-in testing system called CTest. This project teaches you how to define and run tests, manage test-only dependencies, and integrate testing into your development workflow.
Core challenges you’ll face:
- Enabling tests in a project → maps to the
enable_testing()command - Fetching a testing framework → maps to using
FetchContentfor test-only dependencies - Creating a test executable → maps to an
add_executablethat links against your library and the test framework - Defining a test case → maps to the
add_testcommand
Key Concepts:
- CTest: Official CMake Tutorial - Step 4
- GoogleTest Integration: GoogleTest’s own documentation on CMake integration.
Difficulty: Advanced Time estimate: A weekend Prerequisites: Project 4.
Real world outcome: You can now run your tests from the command line and get a clean report of what passed and what failed.
$ cmake --build build
... (output omitted for brevity)
[100%] Built target greeter_tests
$ ctest --test-dir build
Test project /path/to/project/build
Start 1: GreetingTest
1/1 Test #1: GreetingTest ..................... Passed 0.01s
100% tests passed, 0 tests failed out of 1
Implementation Hints:
- In your top-level
CMakeLists.txt, addenable_testing(). - Wrap your
FetchContentcall for GoogleTest in anif(BUILD_TESTING)block, which is a standard CMake option. - Create a new
testssubdirectory with its ownCMakeLists.txt. - In
tests/CMakeLists.txt:- Create a test executable (
greeter_tests). - Link
greeter_testsagainst yourGreetinglibrary and the GoogleTest targets (GTest::gtest_main). - Use
add_test(NAME GreetingTest COMMAND greeter_tests)to register the executable as a test with CTest.
- Create a test executable (
- Write a simple test case in
tests/test_greeting.cppthat checks if your greeting logic works as expected (you might need to refactor your library to return a string instead of printing directly to test it).
Learning milestones:
- The test executable compiles and links → You have correctly integrated the test dependency.
ctestruns the test executable → You have correctly defined the test.- The test passes → Your testing setup is functional.
- Tests are not built if you configure with
-DBUILD_TESTING=OFF→ You have correctly separated test code from production code.
Summary
| Project | Main Language | Difficulty |
|---|---|---|
| The Simplest Executable | C++ | Beginner |
| Creating and Linking a Library | C++ | Beginner |
| Managing Directories and Headers | C++ | Intermediate |
| Adding an External Dependency | C++ | Intermediate |
| Adding Tests with CTest | C++ | Advanced |