← Back to all projects

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.txt files.
  • 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).

  1. Targets: Anything you build.
    • add_executable(...): Creates an executable target.
    • add_library(...): Creates a library target (static or shared).
  2. 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.
  3. 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).
  4. 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.txt file → maps to using cmake_minimum_required, project, and add_executable
  • Running CMake for the first time → maps to the cmake -B build and cmake --build build commands
  • 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:

  1. cmake_minimum_required(VERSION 3.10)
  2. project(Greeter)
  3. add_executable(Greeter main.cpp)

Learning milestones:

  1. CMake generates build files without errors → Your CMakeLists.txt syntax is correct.
  2. The project compiles successfully → CMake correctly found your compiler.
  3. 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_library command
  • 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:

  1. Create greeting.h with a function declaration void print_greeting();.
  2. Create greeting.cpp with the implementation of print_greeting().
  3. Modify main.cpp to #include "greeting.h" and call print_greeting().
  4. Update your CMakeLists.txt to define the two targets and the link dependency between them.

Learning milestones:

  1. Both the library and executable compile → You have defined your targets correctly.
  2. The program runs and prints the message → You have successfully linked the targets.
  3. You understand the difference between add_library and add_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_directories with the PUBLIC keyword
  • 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:

  1. Create the directory structure shown above.
  2. In src/greeting/CMakeLists.txt, define the Greeting library target.
  3. In that same file, use target_include_directories(Greeting PUBLIC .) to specify that consumers of Greeting need to have the current directory (src/greeting) in their include path to find greeting.h.
  4. In the top-level CMakeLists.txt, remove the add_library command.
  5. In its place, use add_subdirectory(src/greeting).
  6. The add_executable and target_link_libraries commands in the top-level file remain the same! CMake automatically makes the Greeting target available.

Learning milestones:

  1. The project builds from the top-level CMakeLists.txtadd_subdirectory is working.
  2. main.cpp can still find greeting.h without .. in the pathtarget_include_directories is correctly propagating usage requirements.
  3. 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_package command and its modes (Config vs. Module)
  • Fetching content at configure time → maps to using FetchContent for a more reproducible build
  • Linking against a third-party target → maps to using the fmt::fmt target that the {fmt} library exports

Key Concepts:

  • FetchContent: Official CMake documentation
  • find_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:

  1. In your top-level CMakeLists.txt, include the FetchContent module.
  2. Declare the {fmt} dependency using FetchContent_Declare, providing its Git repository and a specific version tag.
  3. Call FetchContent_MakeAvailable(fmt). This will download {fmt} and run its CMakeLists.txt, making its targets (like fmt::fmt) available to your project.
  4. In src/greeting/CMakeLists.txt, add fmt::fmt to your target_link_libraries call. The PUBLIC keyword is important here, as it ensures that if another target links against Greeting, it also correctly links against {fmt}.
  5. Update your greeting.cpp to use the {fmt} library.

Learning milestones:

  1. CMake configures and downloads the {fmt} source codeFetchContent is working.
  2. Your Greeting library compiles and links against {fmt} → You are correctly using the imported target.
  3. 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 FetchContent for test-only dependencies
  • Creating a test executable → maps to an add_executable that links against your library and the test framework
  • Defining a test case → maps to the add_test command

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:

  1. In your top-level CMakeLists.txt, add enable_testing().
  2. Wrap your FetchContent call for GoogleTest in an if(BUILD_TESTING) block, which is a standard CMake option.
  3. Create a new tests subdirectory with its own CMakeLists.txt.
  4. In tests/CMakeLists.txt:
    • Create a test executable (greeter_tests).
    • Link greeter_tests against your Greeting library and the GoogleTest targets (GTest::gtest_main).
    • Use add_test(NAME GreetingTest COMMAND greeter_tests) to register the executable as a test with CTest.
  5. Write a simple test case in tests/test_greeting.cpp that 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:

  1. The test executable compiles and links → You have correctly integrated the test dependency.
  2. ctest runs the test executable → You have correctly defined the test.
  3. The test passes → Your testing setup is functional.
  4. 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