CMake for Cross-Platform C++

Modern target-based CMake: usage requirements that propagate correctly, install and export so downstream projects get real imported targets, presets, generators, and the directory-scoped anti-patterns that still dominate legacy CMakeLists files.

Overview

Old CMake vs modern CMake

CMake has been the default answer for cross-platform C++ for a decade, but most codebases contain CMake written before 2014. That style sets global, directory-scoped state (include_directories, link_directories, add_definitions, CMAKE_CXX_FLAGS) that applies to every later target in the directory and its children. It works until targets need different things; then every target gets every flag and nobody knows which are load-bearing.

Per-target usage requirements

Modern CMake (often dated to CMake 3.0 and the target_link_libraries rework) gives each target two sets: what it needs to build itself, and what its consumers need. Linking pulls in the consumer-facing set transitively.

  • PRIVATE — This target's own build only
  • INTERFACE — Consumers only
  • PUBLIC — Both

Every build property has an INTERFACE_ variant, and CMake appends the INTERFACE_ properties of the whole transitive closure onto the consumer. A correct library is consumed with one line: target_link_libraries(app PRIVATE ns::core).

> [!IMPORTANT] Getting PRIVATE, PUBLIC and INTERFACE right is the single highest-leverage skill in CMake.

Distribution is where it pays off

The consumer's CMakeLists is then identical whether your library came from a subdirectory, FetchContent, vcpkg, Conan or a distribution package. That is the difference between a library people use and one they vendor and patch.

Generators

  • Ninja — Single-config — Speed, and compile_commands.json
  • Unix Makefiles — Single-config — The universal fallback
  • Visual Studio, Xcode, Ninja Multi-Config — Multi-config — CMAKE_BUILD_TYPE does not exist

> [!WARNING] Code branching on CMAKE_BUILD_TYPE is silently wrong on multi-config generators. Generator expressions such as $ are portable because they are evaluated at generate time, per configuration.

Presets make it reproducible

CMakePresets.json (CMake 3.19, extended since) puts generator, binary directory, cache variables and toolchain file in a checked-in JSON file read by the command line, Visual Studio, VS Code, CLion and CI. It replaces the shell wrapper that always drifts from what CI runs.

What interviewers probe

  • PUBLIC vs PRIVATE — The public-header test
  • Installable package config — Have you actually written one?
  • file(GLOB) — Why it is dangerous
  • A legacy CMakeLists — Name each anti-pattern and what it breaks

Key concepts

Target
The unit of the modern build model, created by add_executable, add_library (STATIC, SHARED, MODULE, OBJECT, INTERFACE), or add_custom_target. A target owns its sources, its build specifications, and its usage requirements. An ALIAS target such as ns::core provides a stable namespaced name so in-tree and installed consumption look identical.
Usage requirement
A property a target imposes on anything that links to it — include directories, compile definitions, compile features, compile options, link libraries, link options. Stored in the INTERFACE_ variant of the corresponding property and applied transitively across the whole dependency closure at generate time.
PUBLIC / PRIVATE / INTERFACE
Selects where a declaration lands. PRIVATE affects only this target, INTERFACE only its consumers, PUBLIC both. The decision rule is the public-header test: if the dependency appears in your installed headers it is PUBLIC, otherwise PRIVATE. Header-only libraries use INTERFACE for everything because they have no build of their own.
Generator expression
A $ expression evaluated at build-system generation time rather than during CMakeLists processing. Required for anything that varies per configuration or per language, since multi-config generators do not know the configuration at configure time. Cannot be inspected with message(), which is the usual source of confusion.
BUILD_INTERFACE and INSTALL_INTERFACE
Generator expressions that split a usage requirement between the build tree and the installed package. $ content is used only when consuming from the build tree; $ only after install. Without them an exported target carries absolute build-machine paths that do not exist on the consumer.
find_package CONFIG mode
Loads a package-supplied Config.cmake or -config.cmake, typically from lib/cmake// under a prefix in CMAKE_PREFIX_PATH, which defines real imported targets. MODULE mode instead runs a heuristic Find.cmake script. CONFIG is authoritative because the package describes itself; the first viable config file found wins, even if a newer version exists later in the search order.
install(EXPORT) and export()
install(TARGETS ... EXPORT name) associates installed artifacts with an export set; install(EXPORT name NAMESPACE ns:: DESTINATION lib/cmake/pkg) writes the file downstream projects load to import those targets. export() writes the equivalent file for the build tree without installing, which lets a consumer point at a build directory. Use GNUInstallDirs so destinations follow platform convention.
CMakePresets.json
A checked-in JSON file (schema version 1 in CMake 3.19; build and test presets in version 2; package and workflow presets in version 6) defining configurePresets, buildPresets, testPresets, packagePresets, and workflowPresets with inherits, generator, binaryDir, cacheVariables, and condition. CMakeUserPresets.json holds personal presets and is gitignored.

When to use it

  • Any C or C++ project that must build on more than one platform, compiler, or IDE — CMake is the de facto interchange format
  • Publishing a C or C++ library that other teams will consume, where install and export with imported targets is the deliverable
  • Consuming third-party dependencies through vcpkg, Conan, or a distribution package manager, all of which speak find_package in CONFIG mode
  • Standardizing developer and CI build configurations with CMakePresets.json so the two cannot drift apart
  • Enabling clangd, clang-tidy, include-what-you-use, or an IDE index by emitting compile_commands.json from the Ninja or Makefile generator

Interview questions

  • Explain PUBLIC, PRIVATE, and INTERFACE in target_link_libraries and what goes wrong when you get them wrong.
  • Walk through making a library consumable by downstream projects via find_package with imported targets.
  • What are generator expressions, and why is BUILD_INTERFACE versus INSTALL_INTERFACE necessary?
  • Compare the CMake generators, and explain what breaks when you assume CMAKE_BUILD_TYPE.
  • How do you choose between FetchContent, find_package, and a package manager for dependencies?
  • Name the modern CMake anti-patterns and explain what each one actually breaks.

Sign up free to read the full topic

More DevOps topics

Start free on Cariara · Coding problems · System design studio