CMake (C/C++/native)
A cross-platform meta-build system: CMake reads CMakeLists.txt files written in the CMake language and generates native build files (Ninja, Make, Visual Studio, Xcode) — it has no first-class "workspace" concept, so multi-package C/C++ trees are assembled from add_subdirectory, FetchContent, ExternalProject, and CMakePresets.json rather than a declared member set.
| Field | Value |
|---|---|
| Language | C++ (the tool); the CMake scripting language (for CMakeLists.txt) |
| License | BSD-3-Clause (OSI-approved) |
| Repository | Kitware/CMake · upstream gitlab.kitware.com/cmake/cmake |
| Documentation | cmake.org/cmake/help/latest · Using Dependencies Guide |
| Category | Native Build System |
| Workspace model | None native. Single directory tree rooted at one CMakeLists.txt; multi-package via add_subdirectory aggregation, FetchContent/ExternalProject for external trees, and CMakePresets.json for build/test/workflow profiles |
| First released | 2000 (Kitware, for the Visible Human / Insight Toolkit projects) |
| Latest release | 4.3.3 |
Latest release:
4.3.3(released May 21, 2026; the4.3.xseries shipped first-class Common Package Specification (CPS) import/export, build profiling, and schema version 11 forCMakePresets.json). CMake dropped the2.x/3.xnumbering to4.0in early 2025; thecmake-presets(7)schema and thecmake --workflowdriver are the closest thing CMake has to a workspace-orchestration surface. See CLI / UX ergonomics.
Overview
What it solves
CMake's job is build-system generation, not build execution. You describe targets (add_library, add_executable), their sources, their usage requirements (include dirs, compile definitions, link libraries — propagated via PUBLIC/PRIVATE/INTERFACE on target_* commands), and CMake emits a native build for whichever generator you pick (Ninja, Unix Makefiles, Visual Studio 17 2022, Xcode). The actual compile/link is then run by that backend — Ninja, Make, MSBuild — not by CMake itself. This two-phase split (configure → generate, then build) is the defining fact about CMake and the reason its "workspace story" looks unlike a package manager's: there is no resolver, no lockfile, and no manifest array of members.
A C/C++ "monorepo" under CMake is therefore not a declared workspace but an assembled one. A single root CMakeLists.txt calls project() once and then pulls every sub-component into one configure/build graph with add_subdirectory(). Every target across every subdirectory lands in one target dependency graph, one build tree, and one compile_commands.json. Where Cargo (see ../cargo/) has [workspace] members = […] and a virtual root, CMake has a procedural script that add_subdirectorys its way down a tree. The unit of cross-package reference is a target name (target_link_libraries(app PRIVATE mylib)), resolved by CMake's in-process namespace, not a version range.
Design philosophy
CMake is a generator first and an aggregator second. Its dependency story is built around making external content part of the same configure step rather than orchestrating sub-builds. From the actual FetchContent module (Modules/FetchContent.cmake, .rst header):
"This module enables populating content at configure time via any method supported by the
ExternalProjectmodule. WhereasExternalProject_Adddownloads at build time, theFetchContentmodule makes content available immediately, allowing the configure step to use the content in commands likeadd_subdirectory,includeorfileoperations. … Content population details should be defined separately from the command that performs the actual population. This separation ensures that all the dependency details are defined before anything might try to use them to populate content. This is particularly important in more complex project hierarchies where dependencies may be shared between multiple projects."
Three consequences flow from this, and they define CMake's place in this survey:
- One configure, one graph.
add_subdirectory()andFetchContent_MakeAvailable()both splice another project's targets into the current build. There is no isolation boundary: aFetchContented dependency's targets are visible to, and link directly against, the consuming targets — a deliberate contrast with the isolated symlink trees of pnpm or the content-addressed sandboxes of Bazel. - "First to declare, wins." Because everything is one namespace, shared transitive dependencies are de-duplicated by declaration order, not by a version solver: "The first details to be declared for a given dependency take precedence, regardless of where in the project hierarchy that occurs" (
FetchContent). This is CMake's substitute for dependency hoisting. - Profiles, not members. What CMake added to standardize multi-configuration developer workflows is
CMakePresets.json— namedconfigurePresets/buildPresets/testPresets/packagePresets/workflowPresets— and thecmake --workflowdriver to chain them. This is a UX layer over the single project, not a declaration of multiple sibling packages.
CMake sits in the "Native Build System" family alongside Meson, SCons, Waf, and the generator it most often drives, Ninja. Among polyglot engines it is the closest mainstream analogue to GN (also a Ninja-generating meta-build, with a stricter, non-Turing-complete language). For the D-language framing of why a generator-style model maps poorly onto a package manager like dub, see d-landscape.
How it works
Targets, usage requirements, and the configure→build split
A minimal library + executable in one tree:
# libs/mylib/CMakeLists.txt
add_library(mylib STATIC src/mylib.c)
target_include_directories(mylib PUBLIC include) # propagated to consumers
target_compile_features(mylib PUBLIC c_std_11)# apps/app/CMakeLists.txt
add_executable(app src/main.c)
target_link_libraries(app PRIVATE mylib) # cross-package ref = a target nameThe PUBLIC/PRIVATE/INTERFACE keywords on target_link_libraries and target_include_directories are CMake's usage-requirement system: PUBLIC requirements propagate to anything that links the target; PRIVATE stay local; INTERFACE apply only to consumers. This is how a library's include paths and compile flags flow to dependents without a manifest — it is encoded in the target graph itself.
Invoking CMake is two phases:
cmake -S . -B build -G Ninja # CONFIGURE: run the CMakeLists, generate build.ninja
cmake --build build --parallel # BUILD: hand off to Ninja, which runs the compiles
ctest --test-dir build -j8 # TEST: run the registered testsThe configure step writes a CMakeCache.txt (the persisted cache of option()/-D variables) and a compile_commands.json (if CMAKE_EXPORT_COMPILE_COMMANDS=ON) covering every target in the tree.
add_subdirectory: the monorepo aggregator
The canonical "monorepo" idiom is a root CMakeLists.txt that conditionally add_subdirectorys each component. This real example is clay/CMakeLists.txt from the Clay layout library, which aggregates a library plus ~15 example sub-projects:
# clay/CMakeLists.txt (abridged, verbatim)
cmake_minimum_required(VERSION 3.27)
project(clay)
option(CLAY_INCLUDE_ALL_EXAMPLES "Build all examples" ON)
option(CLAY_INCLUDE_RAYLIB_EXAMPLES "Build raylib examples" OFF)
if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_CPP_EXAMPLE)
add_subdirectory("examples/cpp-project-example")
endif()
if(CLAY_INCLUDE_ALL_EXAMPLES OR CLAY_INCLUDE_RAYLIB_EXAMPLES)
add_subdirectory("examples/raylib-multi-context")
add_subdirectory("examples/raylib-transitions")
endif()Each add_subdirectory pulls that directory's CMakeLists.txt into the same configure run, so its targets (clay_examples_raylib_transitions, …) join the one global target graph. The option() gates are CMake's hand-rolled equivalent of a --filter: members are included/excluded by cache variables, evaluated at configure time, not by a workspace member list.
FetchContent: external trees spliced into the build
For dependencies that live outside the tree, FetchContent downloads them at configure time and (optionally) add_subdirectorys them into the same build. Real example, clay/examples/raylib-transitions/CMakeLists.txt:
# clay/examples/raylib-transitions/CMakeLists.txt (verbatim)
include(FetchContent)
FetchContent_Declare(
raylib
GIT_REPOSITORY "https://github.com/raysan5/raylib.git"
GIT_TAG "5.5"
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(raylib) # clones + add_subdirectory(raylib)
add_executable(clay_examples_raylib_transitions main.c)
target_link_libraries(clay_examples_raylib_transitions PUBLIC raylib) # links raylib's targetFetchContent_MakeAvailable(raylib) "will also add them to the main build, if possible, so that the main build can use the populated projects' targets" (FetchContent). After it returns, raylib is an ordinary target you can target_link_libraries against — no separate install, no version resolution, just a target in the same graph. Compare ExternalProject, which keeps the dependency as a build-time sub-build behind an opaque install prefix (used for cross-toolchain or non-CMake deps).
CMakePresets.json: named profiles + cmake --workflow
CMakePresets.json (schema version 11 as of CMake 4.3) standardizes the configure/build/test invocations a project supports. It has five preset arrays — configurePresets, buildPresets, testPresets, packagePresets, workflowPresets — and supports inherits (within a file/its includes) and include (other preset files). A CMakeUserPresets.json implicitly includes CMakePresets.json for personal overrides. Excerpt adapted from llvm/CMakePresets.json:
{
"version": 11,
"configurePresets": [
{
"name": "llvm-export-compile-commands",
"hidden": true,
"cacheVariables": { "CMAKE_EXPORT_COMPILE_COMMANDS": true }
},
{
"name": "release",
"inherits": ["llvm-export-compile-commands"],
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/release",
"cacheVariables": { "CMAKE_BUILD_TYPE": "Release" }
}
],
"buildPresets": [{ "name": "release", "configurePreset": "release" }],
"testPresets": [{ "name": "release", "configurePreset": "release" }],
"workflowPresets": [
{
"name": "release",
"steps": [
{ "type": "configure", "name": "release" },
{ "type": "build", "name": "release" },
{ "type": "test", "name": "release" }
]
}
]
}cmake --workflow --preset release # runs configure → build → test in one driver callcmake --workflow --preset <name> is the closest CMake comes to a task pipeline: it sequences the configure/build/test steps of a single project. It is not a topological multi-package scheduler — there is no notion of "build member A before member B" beyond the implicit ordering inside the one target graph.
Workspace declaration and topology
CMake has no workspace declaration. There is no members array, no glob of sub-packages, no virtual root, and no separate "workspace manifest" distinct from a package manifest. The discovery model is procedural and single-rooted:
- One
project()per configure. The rootCMakeLists.txtcallsproject(name)exactly once (sub-projects may callproject()again, mostly forExternalProject-style independence, but the dominant idiom is a single top-levelproject()). The "workspace" is whatever that one script reaches viaadd_subdirectory(). - Aggregation, not enumeration. Sub-packages are discovered by executing
add_subdirectory()calls — frequently guarded byoption()flags (as inclay/CMakeLists.txt) or computed withfile(GLOB …)/foreach(). There is no declarative member set the tool reads ahead of time; the topology is whatever the script builds at configure time. - No multi-root standard. CMake natively assumes a single source tree. Multi-root support (multiple independent top-level
CMakeLists.txt) lives in IDE tooling, not core CMake: Visual Studio uses aCMakeWorkspaceSettings.jsonand VS Code's CMake Tools extension adds "CMake: Configure All Projects" / "Build All Projects" commands — both external to thecmakeCLI. Thecmake-presets(7)schema has noworkspacePresets.
IMPORTANT
The takeaway for dub: CMake demonstrates the aggregation model (one root that pulls members into one graph) without any of the declarative machinery — no member globs, no virtual workspace, no exclusion list. Everything a workspace would declare statically, CMake computes procedurally at configure time. This is maximally flexible and minimally introspectable: you cannot ask CMake "what are the members?" without running the configure step.
Dependency handling and isolation
CMake's model is maximal sharing, zero isolation — the opposite end of the spectrum from pnpm's isolated symlink store or Bazel's sandboxes.
- Internal cross-references are target names. Member A depends on member B by writing
target_link_libraries(A PRIVATE B)— whereBis a target already defined elsewhere in the same configure run. No path, no version, no manifest: just an identifier resolved in CMake's global target namespace. Namespaced aliases (add_library(Foo::bar ALIAS bar)) andEXPORTsets make this robust acrossfind_packageboundaries. - External dependencies, three tiers.
find_package(Foo)— locate an already-installed package via itsFooConfig.cmake(or, since CMake4.3, a Common Package Specification.cpsJSON file). System-level, no fetching.FetchContent— clone/download at configure time and splice into the same build (one graph; targets directly linkable). This is the de-facto "vendored-source dependency" mechanism.ExternalProject— clone/build at build time as an isolated sub-build behind an install prefix; the only tier that offers real isolation (separate toolchain, separate build tree), at the cost of an opaque boundary.
- No hoisting, no lockfile — "first to declare, wins." With
FetchContent, a diamond where two members both wantzlibis resolved by declaration order: the firstFetchContent_Declare(zlib …)reached wins, and later declarations are ignored. There is no SAT/PubGrub solver and nodub.selections.json-style lock; reproducibility relies on pinnedGIT_TAG/URL_HASHvalues in eachFetchContent_Declare. - Dependency providers (
cmake_language(SET_DEPENDENCY_PROVIDER …)) let a user (e.g. vcpkg, Conan) intercept everyfind_package/FetchContent_MakeAvailableand substitute a managed package — the official seam through which external package managers integrate.OVERRIDE_FIND_PACKAGEmakes aFetchContented dependency satisfy laterfind_package(Foo)calls. These are the closest CMake gets to a unified resolution policy, and they are opt-in glue, not a built-in resolver.
NOTE
Because FetchContent adds the dependency's targets to the main build, a CMake "monorepo" and its FetchContented externals share one build tree and one set of compile flags. This eliminates the redundant-compilation problem dub has with path= cross-refs — but only because CMake refuses to isolate anything. It is hoisting taken to its logical extreme: a single flat target namespace.
Task orchestration and scheduling
CMake's orchestration is delegated to the generated backend, and its "DAG" is a target DAG, not a task DAG.
- The build DAG is the target graph. Every
add_executable/add_libraryand thetarget_link_librariesedges between them form a dependency graph. CMake topologically encodes this into the generatedbuild.ninja(or Makefiles), and Ninja/Make does the scheduling — including all parallelism. CMake itself never compiles anything. - Concurrency is the backend's.
cmake --build build --parallel [N](or-j N) forwards a job count to Ninja/Make, which run independent compile/link legs concurrently. Ninja's scheduler, not CMake, decides ordering and parallelism; CMake's contribution is the correctly-ordered graph. - Change detection is the backend's, and it is timestamp-based. Ninja and Make rebuild based on file modification times and a recorded command line (Ninja additionally hashes the command in its
.ninja_log). This is not content-addressed input hashing of the kind Turborepo, Nx, or Bazel use; there is no per-target cache key, no "affected-since-git-ref" computation, and no remote cache in core CMake. Touching a header re-triggers everything that includes it (tracked via compiler-emitted depfiles), but moving the tree or changing an unrelated env var can over- or under-rebuild. - No affected-detection. CMake has no
--since <git-ref>and no concept of "members impacted by this change." The granularity of incrementality is whatever the backend's mtime tracking provides over the single, flat target graph.ctestcan filter tests (-R,-E,-L) but does not compute a change-impacted subset. cmake --workflowis sequencing, not a scheduler. It runs the ordered steps of one workflow preset (configure → build → test → package). There is no cross-package topological loop à layarn workspaces foreach— the only ordering CMake knows is inside the one target graph.
WARNING
CMake's reliance on the backend for change detection means it has no build/test caching of its own and no remote execution. The incrementality story is entirely "Ninja/Make + mtimes." For content-hashed or remote-cached native builds, teams reach for Bazel/Buck2 or bolt ccache/sccache underneath the compiler — not anything CMake provides.
Caching and remote execution
- No native build/test cache. Core CMake has no content-addressed action cache, no
--cacheflag, and no per-target cache keys. Incremental rebuilds come solely from the generated backend's timestamp tracking (Ninja.ninja_log, Make rules). Re-configuring from scratch into a freshbinaryDirrebuilds everything. - No remote execution / no REAPI. CMake implements none of the Remote Execution API (REAPI). It cannot dispatch compiles to a build farm or fetch action results from a shared cache. This is the single starkest gap versus the polyglot engines in this survey (Bazel, Buck2, Pants) and the JS orchestrators (Turborepo, Nx).
- Caching is bolted on underneath. The community pattern is
RULE_LAUNCH_COMPILE/CMAKE_<LANG>_COMPILER_LAUNCHER=ccache(orsccache), which inserts a compiler cache below CMake. Withsccacheconfigured against an S3/Redis backend you get a shared compile cache, andsccache --distadds distributed compilation — but this is the launcher's machinery, entirely transparent to CMake, which neither knows nor coordinates it. - CTest dashboards are reporting, not caching. CTest can submit build/test results to a CDash dashboard (
ctest -D Experimental), and CMake4.3added build profiling. These are observability features; they do not cache or skip work.
NOTE
Net: CMake is a graph generator with no caching layer of its own. Where this survey's cache-centric tools key tasks by hashed inputs and share results remotely, CMake leaves all of that to (a) the backend's mtime incrementality and (b) an optional compiler-launcher cache. A dub workspace feature that wants content-hashed task caching cannot borrow it from CMake — only the negative lesson that delegating caching entirely to the backend forecloses cross-machine reuse.
CLI / UX ergonomics
The command boundary is mode-flag-driven on the cmake/ctest binaries, with --preset as the named-profile selector and --target for targeted builds:
| Goal | Invocation | Notes |
|---|---|---|
| Configure (generate) | cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release | -S source, -B build dir, -G generator, -D cache vars |
| Configure via preset | cmake --preset release | Pulls generator/binaryDir/cache vars from CMakePresets.json |
| Build everything | cmake --build build --parallel 8 | Forwards -j8 to Ninja/Make |
| Build one target | cmake --build build --target mylib (-t mylib) | The "targeted" filter — by target name |
| Build via preset | cmake --build --preset release | Build settings from buildPresets |
| Run the workflow | cmake --workflow --preset release | Sequences configure → build → test → package |
| List presets | cmake --list-presets · cmake --workflow --list-presets | Discoverability of named profiles |
| Test | ctest --test-dir build -j8 --preset release | --test-dir avoids cding into the build dir (CMake 4.3+) |
| Filter tests | ctest -R '^unit_' -E slow -L integration | Regex include -R, exclude -E, label -L |
| Install | cmake --install build --prefix /opt/app | Runs the generated install rules |
Observations against this survey's filter-ergonomics axis:
- The "filter" is the target name.
cmake --build build --target mylibis CMake's-p/--filter; there is no member-scoped flag because there are no members — only targets in the one graph. Building "just the backend" means knowing and naming the backend's target(s). --presetis the closest thing to a workspace selector, but it selects a configuration profile of the single project, not a sub-package.inherits/includegive it composition;CMakeUserPresets.jsongives per-developer overrides. It is excellent UX for "here are the supported build configurations" and poor UX for "operate on member X."- No
--since, no recursion flags. Nothing in the CMake/CTest CLI computes a git-diff-impacted set or walks a member sub-graph (--from/--recursivein Yarn terms). The only graph traversal is the implicit target-dependency ordering the backend performs. - Generator choice is a first-class flag.
-G Ninjavs-G "Unix Makefiles"vs-G "Visual Studio 17 2022"— the meta-build identity. Multi-config generators add--config Releaseat build time; single-config generators bakeCMAKE_BUILD_TYPEat configure time.
Strengths
- Ubiquitous and portable. The de-facto standard for C/C++; generates for Ninja, Make, Visual Studio, and Xcode from one
CMakeLists.txt, across Linux/macOS/Windows. - One graph, one
compile_commands.json. Aggregating an entire tree withadd_subdirectoryyields a single target graph, single build tree, and single compile-commands database — clangd/IDE tooling "just works" across the whole monorepo. FetchContentmakes source dependencies trivial. PinnedGIT_TAG+FetchContent_MakeAvailablevendors a dependency's targets directly into your build, no install step, no separate version solve.- Usage requirements propagate correctly.
PUBLIC/PRIVATE/INTERFACEontarget_*commands give transitive include/flag/link propagation without a manifest — modern "target-based" CMake is genuinely composable. CMakePresets.jsonstandardizes invocations. Named, inheritable, shareable build/test/workflow profiles, with--list-presetsfor discoverability andcmake --workflowto chain them.- First-class package-manager seams. Dependency providers (
SET_DEPENDENCY_PROVIDER),OVERRIDE_FIND_PACKAGE, and CPS (4.3) let vcpkg/Conan/CPS integrate cleanly.
Weaknesses
- No workspace concept whatsoever. No member declaration, no virtual root, no glob, no exclusion list, no multi-root in the core CLI; topology is procedurally computed at configure time and not introspectable.
- No caching of its own. No content-addressed action cache, no per-target cache keys; incrementality is entirely the backend's mtime tracking, and a fresh build dir rebuilds everything.
- No remote execution / no REAPI. Cannot dispatch to or fetch from a build farm; distributed/shared caching requires an external compiler launcher (
sccache/ccache). - No affected-detection. No
--since <ref>, no impacted-member computation; the only "filter" is naming targets. - Resolution is "first-declared-wins."
FetchContentdiamonds are resolved by declaration order, not a version solver; reproducibility hinges on hand-pinned tags/hashes with no lockfile. - Turing-complete configure language.
CMakeLists.txtis imperative and stateful (cache variables, global properties, directory scoping), making large trees hard to reason about — the very property GN and Bazel reject by using a restricted language. - Two-phase mental model. The configure/build split and "you generate a build, you don't run one" trips up newcomers and complicates any caching/affected story.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
| Meta-build (generate native files) rather than run the build itself | Portability: one description targets Ninja, Make, MSBuild, Xcode across all OSes | Caching/scheduling/parallelism are the backend's; CMake owns no cache and no remote execution |
No workspace primitive; aggregate via add_subdirectory | Maximal flexibility — any tree shape, computed procedurally; one graph + one compile-commands db | No declarative member set, no virtual root, no introspection; "filter" is just naming a target |
FetchContent splices deps into the same build | Source dependencies become ordinary linkable targets; no install/version-solve round-trip | Zero isolation; shared global target namespace; diamonds resolved by declaration order, not a solver |
| "First to declare, wins" instead of a version resolver | Simple, deterministic given pinned tags; no SAT/PubGrub machinery in the build tool | No lockfile, no conflict detection; reproducibility depends entirely on hand-pinned GIT_TAG/hashes |
| Turing-complete imperative configure language | Express arbitrary platform/feature logic (if, foreach, option, file(GLOB)) | Hard to analyze statically; large trees become stateful and order-dependent (cf. GN's ban) |
Caching delegated to the generator + optional ccache/sccache | Keeps CMake a pure graph generator; lets users choose any compiler-cache backend underneath | No native, no per-target, no remote cache; cross-machine reuse needs external, CMake-opaque tooling |
CMakePresets.json profiles instead of workspace members | Standardize/share the configurations a project supports; composable via inherits/include | Profiles describe one project's build modes, not sibling packages; cmake --workflow ≠ topo scheduler |
| Dependency providers / CPS as integration seams | Let vcpkg/Conan/CPS own resolution without CMake reinventing a package manager | Real multi-package resolution lives outside CMake; without them you are back to "first-declared-wins" |
Sources
- Kitware/CMake — GitHub mirror · upstream GitLab
- CMake documentation (latest, 4.3.3)
cmake(1)manual —--build,--workflow,--preset,--installcmake-presets(7)—configurePresets/buildPresets/testPresets/packagePresets/workflowPresets, schema v11FetchContentmodule documentation — "first to declare, wins",OVERRIDE_FIND_PACKAGEModules/FetchContent.cmake— verbatim.rstheader (configure-time population)- Using Dependencies Guide
- CMake 4.3 release notes — CPS, build profiling, presets v11
- Common Package Specification is Out the Gate (Kitware)
- Licensing for CMake (BSD-3-Clause)
- Real-world configs:
clay/CMakeLists.txt·clay/examples/raylib-transitions/CMakeLists.txt·llvm/CMakePresets.json - Sibling deep-dives: GN · Bazel · Buck2 · Cargo · pnpm · Turborepo · Nx · Yarn Berry · Pants · the umbrella catalog · D-language framing in d-landscape