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.

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.

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.

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.

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.

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.

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.

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.
Common CMake generators and their characteristics
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

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
Step 1 — Write the CMakeLists.txtThe 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
Step 2 — Run the Configure StepFrom 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
Step 3 — Run the Build StepThe 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
Step 4 — Observe Cross-Platform PortabilityA 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.

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.

Comparison of C++ build system tools
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
✦ 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.

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.

How foundational CMake concepts connect to advanced topics
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

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.

Practice Problems

PROBLEM 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.
PROBLEM 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.
PROBLEM 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?
PROBLEM 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?
PROBLEM 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.

Lesson 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)