C++ • Getting Started and Tooling

CMake Purpose — Explain what CMake is used for (conceptual)

Understanding how CMake abstracts and automates the C++ build process across platforms and compilers.

Programming braces with a command terminal and binary digits.
Inside this lesson 10 sections 5 questions 5 figures

SECTION 1

Historical Context & Motivation

Compiling a C++ program has never been as simple as feeding a single source file to a compiler. As projects grew from modest single-file utilities into sprawling multi-library codebases during the 1980s and 1990s, developers confronted a fundamental engineering problem: how do you reliably describe which sources to compile, which libraries to link, and in what order—across different operating systems, compilers, and hardware architectures? The earliest answer was the venerable Make utility on Unix, which used hand-written Makefile scripts to orchestrate the build. Make was powerful but deeply platform-specific: a Makefile written for GNU Make on Linux would not work on Windows without extensive rewriting, and each new compiler or IDE demanded its own project file format.

Large scientific computing projects such as the Insight Segmentation and Registration Toolkit (ITK) at Kitware felt this pain acutely. Their codebase had to compile on Linux, macOS, Windows, Solaris, and several commercial Unix variants, using compilers ranging from GCC to MSVC to the Compaq compiler. Maintaining separate build scripts for every platform was untenable, and it was this pressure that gave rise to CMake—a tool whose entire purpose is to generate the correct, native build files for whatever platform and toolchain you happen to be targeting.

1976 • Make Is Born

Stuart Feldman creates make at Bell Labs, establishing the concept of rule-based, dependency-driven builds. It becomes the standard build tool on Unix but remains tightly coupled to the Unix shell.

1991 • GNU Autotools Emerge

The GNU project ships autoconf and automake, which generate portable Makefiles via shell-based configuration scripts. This improves cross-Unix portability but remains Unix-centric and notoriously complex.

2000 • CMake 1.0 Released

Kitware releases the first public version of CMake to support the ITK project. Unlike Autotools, CMake is designed from the start to target Windows (Visual Studio), Unix (Makefiles), and macOS (Xcode) from a single set of build descriptions.

2014 • Modern CMake (3.x Era)

CMake 3.0 introduces target-based dependency management and generator expressions, marking the shift to what the community calls Modern CMake. Large projects like LLVM, Qt, and KDE migrate their build systems to CMake.

2020s • De Facto Industry Standard

CMake becomes the dominant build system generator in the C++ ecosystem. Package managers like vcpkg and Conan integrate natively with CMake, and virtually every major IDE provides first-class CMake support.

The central question CMake addresses can be stated succinctly: How can a developer describe what to build once and have the correct platform-native build instructions generated automatically? Understanding CMake's answer to this question is the conceptual foundation upon which all practical CMake usage rests.

SECTION 2

Core Principles & Definitions

Before diving into CMake's mechanics, it is essential to distinguish CMake from a compiler or a build tool. CMake does not compile your code. It does not link your object files. Instead, CMake is a meta-build system—or, more precisely, a build system generator. It reads a declarative description of your project (written in CMakeLists.txt files) and produces the native build files—Makefiles, Ninja build files, Visual Studio .sln projects, Xcode projects, and others—that the actual build tool then consumes. This separation of concerns is the single most important idea in CMake's design.

1

Build System Generator

CMake's fundamental role: it generates build files for a native build tool (Make, Ninja, MSBuild, Xcode) rather than performing the build itself. This one layer of indirection is what enables cross-platform portability.
2

Declarative Project Description

Developers express what they want (targets, source files, dependencies, compile options) in CMakeLists.txt files. CMake decides how to translate that intent into platform-specific instructions.
3

Target-Centric Model

In modern CMake, the central abstraction is the target—an executable or library with associated properties (sources, include paths, compile flags, link dependencies). Dependencies between targets propagate transitively.
4

Platform Abstraction

CMake abstracts away platform differences—path separators, compiler flag syntax, shared library conventions—so the same CMakeLists.txt works on Linux with GCC, Windows with MSVC, and macOS with Clang without modification.
5

Out-of-Source Builds

CMake encourages generating build artifacts in a separate directory from the source tree. This keeps the source clean, allows multiple build configurations (Debug, Release) simultaneously, and simplifies version control.
✦

KEY TAKEAWAY

Think of CMake as an architect who reads your blueprints (the CMakeLists.txt) and then writes detailed, locale-specific construction orders for whichever crew is available—a Japanese crew, a German crew, an American crew. The architect doesn't hammer nails; the architect ensures every crew receives instructions they understand. Similarly, CMake doesn't compile code; it translates your project description into the native language of whichever build tool and compiler your platform uses.

SECTION 3

Visual Explanation — The CMake Pipeline

The following diagram illustrates the two-phase workflow that CMake introduces into the C++ development process. In the first phase, called the configure step, CMake reads your CMakeLists.txt files, detects the compiler, resolves dependencies, and generates native build files. In the second phase, the build step, a native build tool (Make, Ninja, MSBuild, etc.) reads those generated files and actually invokes the compiler and linker to produce your executable or library.

PHASE 1 — ConfigurePHASE 2 — BuildCMakeLists.txtCMake Engine(detect compiler, resolve deps)Makefilebuild.ninja.sln / .vcxproj.xcodeprojGenerates one of:Native Build Tool(Make, Ninja, MSBuild, Xcode)Compiler + Linker(GCC, Clang, MSVC)Executable / Librarymyapp, libutil.so, util.dllCMake Two-Phase Build Pipeline
The left phase shows CMake reading CMakeLists.txt and generating platform-native build files. The right phase shows the native build tool invoking the compiler and linker to produce the final binary. CMake operates only in Phase 1; it is not involved in Phase 2.

Notice that the developer interacts primarily with the CMakeLists.txt file on the left side of the diagram. Everything to the right of the CMake engine is automatically generated and platform-specific. A developer on Linux might get a Makefile, while a colleague on Windows using Visual Studio receives a .sln project—both from the identical CMakeLists.txt. This is the core value proposition of CMake.

SECTION 4

How CMake Works — The Configuration and Generation Pipeline

Although CMake is not a mathematically driven tool, its internal pipeline follows a precise, multi-stage algorithm that is worth understanding at the conceptual level. When you invoke cmake -S . -B build, CMake performs a sequence of operations that can be decomposed into four distinct stages: parsing, configuration, generation, and caching. Each stage builds upon the output of the previous one, culminating in the native build files that drive compilation.

Stage 1 — Parsing

CMake reads the top-level CMakeLists.txt and recursively processes any subdirectories included via add_subdirectory(). The CMake scripting language is imperative—commands execute in order—but the overall project description is declarative in spirit. During parsing, CMake builds an internal representation of all declared targets (executables and libraries) and their associated properties (source files, include directories, compile definitions, link dependencies).

Stage 2 — Configuration

CMake probes the host system to detect the available compiler, its version, and supported features. It evaluates find_package() calls to locate third-party libraries, checks for header files, tests compiler capabilities, and resolves all variables and generator expressions. The result is a fully resolved, platform-aware internal model of your project. This is also the stage where the generator (e.g., Unix Makefiles, Ninja, Visual Studio 17 2022) is selected.

Stage 3 — Generation

Using the resolved internal model, CMake writes out the native build files into the build directory. For the Ninja generator, this produces a build.ninja file; for Unix Makefiles, it produces a hierarchy of Makefiles. The build tool can now be invoked independently of CMake—CMake's job is done.

Stage 4 — Caching

CMake stores all configuration results in a file called CMakeCache.txt inside the build directory. On subsequent runs, CMake reads this cache to avoid redundant detection work, making re-configuration fast. Developers can override cached variables via -D flags (e.g., cmake -DCMAKE_BUILD_TYPE=Release ..) or through GUI tools like ccmake and cmake-gui.

💡

Why "Out-of-Source" Builds Matter

Because CMake writes all generated files into a separate build directory, your source tree remains pristine. You can delete the entire build directory at any time and regenerate it from scratch with a single cmake invocation. This also allows you to maintain multiple build directories simultaneously—one for Debug, one for Release, one for a sanitizer build—each generated from the same CMakeLists.txt.

SECTION 5

Generators & Platform Targets

The concept of a generator is central to CMake's architecture. A generator is the backend that determines the format of the build files CMake outputs. Each generator corresponds to a specific native build tool, and the choice of generator dictates how your project is built, debugged, and packaged on a given platform. The following diagram maps the most common generators to their platforms and build tools.

CMake Generator EcosystemCMakeLists.txtYour project descriptionUnix MakefilesLinux / macOS / BSDNinjaAll platformsVisual Studio 17WindowsXcodemacOS / iOSmake / gmakeninjaMSBuildxcodebuildGCC / ClangGCC / Clang / MSVCMSVC / Clang-clApple ClangGeneratorsBuild ToolsCompilers
The three-layer hierarchy: CMake generators produce build files for native build tools, which in turn invoke platform-specific compilers. The Ninja generator is notable for its cross-platform availability and speed.
GeneratorOutput FormatTypical PlatformKey Advantage
Unix MakefilesMakefile hierarchyLinux, macOS, BSDUniversally available on Unix-like systems
Ninjabuild.ninjaAll platformsFastest incremental builds; parallelism by default
Visual Studio 17 2022.sln + .vcxprojWindowsNative IDE integration; multi-config (Debug/Release)
Xcode.xcodeprojmacOS, iOSFull Xcode IDE integration; code signing support

Common CMake generators and their characteristics

SECTION 6

Worked Example — From CMakeLists.txt to Binary

Let us trace through a concrete, minimal example to solidify the conceptual model. Suppose you have a small C++ project with a main.cpp that depends on a utility library defined in mathutil.h and mathutil.cpp. We will walk through how a developer would describe this project in CMake and what CMake produces at each stage.

Building a Simple C++ Project with CMake

  1. 1

    Step 1 — Write the CMakeLists.txt

    The developer creates a CMakeLists.txt in the project root. The file declares the minimum CMake version, names the project, creates a static library target called mathutil from mathutil.cpp, and creates an executable target called calculator from main.cpp that links against mathutil. The key commands are: cmake_minimum_required(), project(), add_library(), add_executable(), and target_link_libraries().
    A single, platform-independent CMakeLists.txt describing two targets and their relationship.
  2. 2

    Step 2 — Run the Configure Step

    From the project root, the developer runs cmake -S . -B build -G Ninja. The -S . flag points to the source directory, -B build specifies an out-of-source build directory, and -G Ninja selects the Ninja generator. CMake detects the compiler (e.g., g++ 13.2), verifies that C++ compilation works, and writes build.ninja along with CMakeCache.txt into the build/ directory.
    Generated files: build/build.ninja, build/CMakeCache.txt, and supporting CMake metadata.
  3. 3

    Step 3 — Run the Build Step

    The developer runs cmake --build build (or equivalently, ninja -C build). Ninja reads build.ninja, determines the dependency graph, and invokes g++ to compile mathutil.cpp into libmathutil.a, compile main.cpp into an object file, and link everything into the calculator executable. Note that CMake itself is not involved in this step—Ninja and g++ do all the work.
    Final output: build/calculator — a runnable executable.
  4. 4

    Step 4 — Observe Cross-Platform Portability

    A teammate on Windows clones the same repository and runs cmake -S . -B build -G "Visual Studio 17 2022". CMake generates a .sln solution with .vcxproj files instead of build.ninja. The teammate opens the solution in Visual Studio and builds with MSBuild and MSVC. The same CMakeLists.txt produced correct build instructions on both platforms without any modification.
    One CMakeLists.txt → two platforms → two different native build systems → same correct binary output.

SECTION 7

CMake vs. Alternatives — Strengths and Limitations

CMake is not the only build system or build system generator available to C++ developers. Understanding where CMake excels and where it has limitations provides important context for evaluating it against alternatives. The table below compares CMake with several other tools commonly encountered in the C++ ecosystem, including raw Makefiles, Autotools, Meson, and Bazel.

ToolTypeCross-PlatformKey StrengthKey Limitation
CMakeBuild system generatorExcellent (Linux, Windows, macOS, embedded)Massive ecosystem, IDE integration, target-based modelScripting language is non-standard; legacy projects can be hard to read
MakeBuild systemUnix only (without Cygwin/MSYS)Ubiquitous on Unix; simple for small projectsNo portability; manual dependency tracking; error-prone syntax
AutotoolsBuild system generatorUnix variants (poor Windows support)Mature; excellent at detecting Unix system featuresExtremely complex; virtually no Windows support; slow configure
MesonBuild system generatorGood (generates Ninja files)Clean, Pythonic syntax; fast; modern designSmaller ecosystem; less IDE support; fewer generators
BazelBuild systemGoodHermetic builds; excellent caching; scales to monoreposSteep learning curve; heavy infrastructure; not C++-specific

Comparison of C++ build system tools

✦

WHY CMAKE DOMINATES

CMake's dominance is not necessarily because its scripting language is the most elegant—it is widely acknowledged to be quirky. Rather, CMake wins on ecosystem breadth: every major IDE (CLion, Visual Studio, VS Code, Qt Creator) supports CMake natively; every major package manager (vcpkg, Conan) integrates with CMake; and the vast majority of open-source C++ libraries ship CMake build files. Choosing CMake means choosing compatibility with the rest of the C++ world.

SECTION 8

Connection to Advanced Build Concepts

Understanding CMake's purpose as a build system generator prepares you for several advanced topics that you will encounter as your C++ projects grow in complexity. CMake's target-based dependency model, for example, scales naturally into sophisticated scenarios involving cross-compilation for embedded devices, packaging and installing libraries for distribution, and integrating third-party dependencies through package managers. The table below previews how the core concepts discussed in this lesson map onto more advanced CMake features.

Concept from This LessonAdvanced ExtensionWhat You'll Learn Next
Targets (executables, libraries)Transitive dependency propagationUsing PUBLIC, PRIVATE, INTERFACE to control how properties flow between targets
Generators (Ninja, MSVC, Xcode)Toolchain files and cross-compilationUsing CMAKE_TOOLCHAIN_FILE to target ARM, RISC-V, or WebAssembly from a desktop host
find_package() during configurationPackage managers (vcpkg, Conan)Automatic dependency resolution and versioning via CMake integration points
Out-of-source buildsCI/CD pipelines and CTestAutomated testing (ctest) and packaging (cpack) in continuous integration environments
Caching (CMakeCache.txt)CMake presetsSharing standardized build configurations across teams via CMakePresets.json

How foundational CMake concepts connect to advanced topics

The essential insight to carry forward is that CMake's role as a layer of abstraction above the build tool is not merely a convenience—it is a prerequisite for the sophisticated multi-platform, multi-compiler, multi-configuration workflows that professional C++ development demands. Every advanced CMake feature is, at its core, an extension of the same principle you learned today: describe what you want, and let CMake figure out how to achieve it on the target platform.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

A classmate claims that "CMake compiles your C++ code." Explain precisely why this statement is incorrect, and describe what CMake actually does in the build process. Use the terms build system generator, native build tool, and compiler in your answer.
Show answer
CMake is a build system generator, not a compiler. It reads CMakeLists.txt files and produces native build files (such as Makefiles or Visual Studio projects) that a native build tool (such as Make, Ninja, or MSBuild) can consume. The native build tool then invokes the actual compiler (GCC, Clang, or MSVC) to translate C++ source code into machine code. CMake operates only during the configure and generate phases; it plays no role during compilation or linking. In short, CMake tells the build tool what to compile and how; the build tool tells the compiler to do the work.

Question 2: BASIC CALCULATION

You have a project that needs to be built on three platforms: Linux (using GCC and Make), Windows (using MSVC and Visual Studio), and macOS (using Clang and Xcode). Without CMake, how many separate, platform-specific build configurations would you need to maintain? With CMake, how many CMakeLists.txt files describe the project? Explain the practical implication of this difference.
Show answer
Without CMake, you would need to maintain three separate build configurations: a Makefile for Linux/GCC, a .sln/.vcxproj for Windows/MSVC, and a .xcodeproj for macOS/Clang. With CMake, a single CMakeLists.txt (or set of CMakeLists.txt files for subdirectories) describes the project once. CMake generates the correct platform-native files for each platform. The practical implication is a dramatic reduction in maintenance burden: when you add a new source file or dependency, you update one file instead of three, eliminating the risk of configurations drifting out of sync.

Question 3: INTERMEDIATE

Explain the difference between a single-configuration generator (like Unix Makefiles or Ninja) and a multi-configuration generator (like Visual Studio or Xcode). Where is the build type (Debug vs. Release) specified in each case, and what is the implication for out-of-source build directories?
Show answer
A single-configuration generator (e.g., Unix Makefiles, Ninja) locks in the build type at configure time via -DCMAKE_BUILD_TYPE=Debug. To have both Debug and Release builds, you create two separate build directories. A multi-configuration generator (e.g., Visual Studio, Xcode) defers the build type choice to build time: the generated project file contains configurations for Debug, Release, RelWithDebInfo, etc., and the user selects one when building. This means a single build directory can house all configurations. The implication is that with single-config generators, developers often maintain parallel build directories (e.g., build-debug/ and build-release/), while multi-config generators handle this within one directory.

Question 4: APPLIED

You are integrating a third-party image processing library called libimgproc into your CMake project. The library ships with a CMake config file (imgprocConfig.cmake). At a conceptual level, describe what happens during CMake's configure step when your CMakeLists.txt contains find_package(imgproc REQUIRED). What information does CMake extract, and how does this relate to the target-based model?
Show answer
When CMake encounters find_package(imgproc REQUIRED), it searches standard installation paths for imgprocConfig.cmake (or a Find module). Once found, it executes the config file, which creates imported targets (e.g., imgproc::imgproc) that encapsulate the library's include directories, link libraries, compile definitions, and any transitive dependencies. Your project then uses target_link_libraries(myapp PRIVATE imgproc::imgproc) to consume all of these properties automatically. This is the target-based model in action: instead of manually specifying -I and -l flags, the dependency's properties propagate through the target graph, and CMake translates them into the correct compiler and linker flags for the active generator.

Question 5: CRITICAL THINKING

CMake introduces an extra layer of indirection between the developer and the compiler. Some developers argue that this indirection adds unnecessary complexity—why not just write Makefiles or use an IDE's native project format directly? Construct a reasoned argument for when CMake's indirection is genuinely valuable and when it might be overkill. Consider factors such as team size, target platform count, project lifespan, and dependency complexity.
Show answer
CMake's indirection is genuinely valuable when: (1) the project targets multiple platforms or compilers, because maintaining N separate build configurations is more error-prone and costly than maintaining one CMakeLists.txt; (2) the team is large or distributed, because CMake provides a single source of truth that all developers and CI systems consume regardless of their local toolchain; (3) the project has non-trivial dependencies, because CMake's find_package and target-based model automate dependency propagation that would otherwise require fragile manual flag management; and (4) the project has a long lifespan, because build system migrations are expensive and CMake's generator abstraction buffers against changes in IDE versions or build tool preferences. Conversely, CMake may be overkill for a single-file utility on one platform, a quick prototype, or a project deeply embedded in a single IDE's ecosystem (e.g., a purely iOS app that will never leave Xcode). The critical insight is that the cost of CMake's indirection is paid primarily during initial setup and learning, while its benefits compound over the lifetime of a project—making it a net positive for any non-trivial, long-lived C++ codebase.

SUMMARY

Lesson Summary

Summary

CMake is a build system generator that reads declarative project descriptions written in CMakeLists.txt files and produces platform-native build files for tools like Make, Ninja, MSBuild, and Xcode. It does not compile code itself; rather, it abstracts away platform and compiler differences so that a single project description works across Linux, Windows, and macOS without modification. The two-phase workflow—configure then build—cleanly separates project description from compilation, with CMake operating exclusively in the first phase.

Modern CMake organizes projects around targets—executables and libraries with associated properties that propagate transitively through the dependency graph. CMake's generator abstraction (Unix Makefiles, Ninja, Visual Studio, Xcode) is what enables true cross-platform portability, while out-of-source builds keep source trees clean and allow multiple build configurations simultaneously. CMake's dominance in the C++ ecosystem stems from its unmatched IDE integration, package manager support, and community adoption, making it the standard choice for any non-trivial C++ project.

Varsity Tutors • C++ • CMake Purpose — Explain what CMake is used for (conceptual)