Target-Centric Philosophy
If you take one thing from this article, let it be this: modern CMake is about targets and their properties, not variables. Every build requirement — include directories, compiler flags, definitions, linked libraries — attaches to a target. Targets carry their requirements with them, and those requirements propagate automatically to consumers.
CMAKE_CXX_FLAGS, include_directories()) is the CMake equivalent of global state — fragile and unpredictable at scale.
What is a Target?
In CMake, a target is a named build artifact with attached properties. There are several kinds (documented in the cmake-buildsystem(7) manual):
| Target Type | Created By | Output |
|---|---|---|
| Executable | add_executable() | Binary program |
| Static Library | add_library(name STATIC ...) | .a / .lib |
| Shared Library | add_library(name SHARED ...) | .so / .dll / .dylib |
| Object Library | add_library(name OBJECT ...) | Object files (no archive) |
| Interface Library | add_library(name INTERFACE) | No output — pure requirements |
| Imported Target | add_library(name IMPORTED ...) | Pre-built external artifact |
| Alias Target | add_library(ns::name ALIAS real) | Reference to another target |
| Custom Target | add_custom_target() | Runs arbitrary commands |
Here's the contrast between old-style (variable-based) and modern (target-based) CMake:
# ============================================================
# OLD STYLE: Global variables (DON'T DO THIS)
# ============================================================
cmake_minimum_required(VERSION 3.21)
project(OldStyle LANGUAGES CXX)
# Global include directory — affects ALL targets
include_directories(${PROJECT_SOURCE_DIR}/include)
# Global compile flags — affects ALL targets
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra")
# Global definitions — affects ALL targets
add_definitions(-DUSE_FEATURE_X)
add_library(mylib src/mylib.cpp)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp mylib)
# ============================================================
# MODERN STYLE: Target properties (DO THIS)
# ============================================================
cmake_minimum_required(VERSION 3.21)
project(ModernStyle LANGUAGES CXX)
add_library(mylib src/mylib.cpp)
# Attach requirements to the target that needs them
target_include_directories(mylib
PUBLIC ${PROJECT_SOURCE_DIR}/include # Consumers also need this
PRIVATE ${PROJECT_SOURCE_DIR}/src # Only mylib needs this
)
target_compile_definitions(mylib PUBLIC USE_FEATURE_X)
target_compile_options(mylib PRIVATE -Wall -Wextra)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)
# myapp automatically gets include dirs and definitions from mylib!
Target Properties
Every target has a set of properties — key-value pairs that control how it's built. CMake defines hundreds of built-in properties (see cmake-properties(7)), and you can also create custom ones.
Setting and Getting Properties
There are two main ways to manipulate target properties:
cmake_minimum_required(VERSION 3.21)
project(TargetProperties LANGUAGES CXX)
add_executable(myapp main.cpp)
# Method 1: set_target_properties (low-level, any property)
set_target_properties(myapp PROPERTIES
OUTPUT_NAME "my-application"
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
# Method 2: target_* commands (preferred for build requirements)
target_compile_features(myapp PRIVATE cxx_std_20)
target_compile_definitions(myapp PRIVATE APP_VERSION="1.0.0")
target_include_directories(myapp PRIVATE ${PROJECT_SOURCE_DIR}/include)
# Reading properties (useful in debugging/conditional logic)
get_target_property(app_output myapp OUTPUT_NAME)
message(STATUS "Output name: ${app_output}")
get_target_property(app_sources myapp SOURCES)
message(STATUS "Sources: ${app_sources}")
target_* commands (target_compile_definitions, target_include_directories, target_link_libraries, etc.) for build requirements that may propagate to consumers. Use set_target_properties for metadata that doesn't propagate — like OUTPUT_NAME, FOLDER, RUNTIME_OUTPUT_DIRECTORY, or VERSION.
Common Target Properties
cmake_minimum_required(VERSION 3.21)
project(PropertyDemo LANGUAGES CXX)
add_library(engine SHARED src/engine.cpp)
set_target_properties(engine PROPERTIES
# Output control
OUTPUT_NAME "game_engine" # Output: libgame_engine.so (not libengine.so)
PREFIX "" # Remove "lib" prefix: game_engine.so
SUFFIX ".plugin" # Custom suffix: game_engine.plugin
VERSION 2.1.0 # SONAME versioning: libengine.so.2.1.0
SOVERSION 2 # SONAME link: libengine.so.2
# Build settings
POSITION_INDEPENDENT_CODE ON # -fPIC
VISIBILITY_INLINES_HIDDEN ON # Hide inline function symbols
CXX_VISIBILITY_PRESET hidden # Default symbol visibility
# Organization (IDE)
FOLDER "Libraries/Core" # Visual Studio / Xcode folder
# Output directories
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
)
PUBLIC vs PRIVATE vs INTERFACE
The three visibility keywords control who sees a build requirement. This is the most important concept in modern CMake. The Target Usage Requirements documentation covers this in detail.
flowchart TB
subgraph "Library Target (mylib)"
direction TB
PRIV["PRIVATE requirements
Used ONLY to build mylib"]
PUB["PUBLIC requirements
Used to build mylib AND
propagated to consumers"]
IFACE["INTERFACE requirements
NOT used to build mylib
Only propagated to consumers"]
end
subgraph "Consumer Target (myapp)"
direction TB
CONS["Inherited requirements
from PUBLIC + INTERFACE"]
end
PUB -->|propagates| CONS
IFACE -->|propagates| CONS
PRIV -.->|does NOT propagate| CONS
PRIVATE — Internal Implementation Only
Use PRIVATE for requirements that are needed only when building the target itself. They do not propagate to anything that links against this target.
cmake_minimum_required(VERSION 3.21)
project(PrivateDemo LANGUAGES CXX)
add_library(json_parser src/json_parser.cpp)
# PRIVATE: Only json_parser needs these to compile
target_include_directories(json_parser
PRIVATE ${PROJECT_SOURCE_DIR}/src/detail # Internal headers
)
target_compile_definitions(json_parser
PRIVATE PARSER_INTERNAL_BUFFER_SIZE=4096 # Implementation detail
)
target_compile_options(json_parser
PRIVATE -Wall -Wextra -Wpedantic # Warnings for this target only
)
# Consumer does NOT see detail/ headers, definitions, or warnings
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE json_parser)
PUBLIC — Implementation AND Interface
Use PUBLIC when both the library itself and its consumers need the requirement. This is common for include directories that expose headers used in the library's public API.
cmake_minimum_required(VERSION 3.21)
project(PublicDemo LANGUAGES CXX)
add_library(math_utils src/math_utils.cpp)
# PUBLIC: math_utils needs include/ to compile,
# AND consumers need include/ to use math_utils.h
target_include_directories(math_utils
PUBLIC ${PROJECT_SOURCE_DIR}/include # Public API headers
PRIVATE ${PROJECT_SOURCE_DIR}/src/detail # Internal helpers
)
# PUBLIC: consumers that use math_utils also need to know about SSE
target_compile_definitions(math_utils
PUBLIC MATH_USE_SSE2 # API depends on SSE types
PRIVATE MATH_INTERNAL_OPT # Implementation-only flag
)
add_executable(calculator src/main.cpp)
target_link_libraries(calculator PRIVATE math_utils)
# calculator automatically gets:
# - include/ in its include path
# - MATH_USE_SSE2 defined
# calculator does NOT get:
# - src/detail/ in include path
# - MATH_INTERNAL_OPT defined
INTERFACE — Consumer-Only Requirements
Use INTERFACE when a requirement applies only to consumers, not to the target itself. This is essential for header-only libraries and interface libraries.
cmake_minimum_required(VERSION 3.21)
project(InterfaceDemo LANGUAGES CXX)
# Header-only library: no .cpp to compile, so no PRIVATE/PUBLIC needed
add_library(header_math INTERFACE)
target_include_directories(header_math
INTERFACE ${PROJECT_SOURCE_DIR}/include # Only consumers need this
)
target_compile_features(header_math
INTERFACE cxx_std_20 # Consumers must compile with C++20
)
target_compile_definitions(header_math
INTERFACE HEADER_MATH_VERSION=2
)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE header_math)
# myapp gets include/, C++20, and HEADER_MATH_VERSION=2
#include from a library exposes types/symbols that also appear in the library's own public headers → the dependency is PUBLIC. If you only use it internally and never expose it in your API → it's PRIVATE. If you are a header-only or interface library with nothing to compile → everything is INTERFACE.
Transitive Dependencies
Transitive (or "usage requirement") propagation is what makes target-centric CMake powerful. When target A links against target B with PUBLIC requirements, anything that links against A also inherits B's PUBLIC requirements — automatically, recursively.
Propagation Rules
cmake_minimum_required(VERSION 3.21)
project(TransitiveDeps LANGUAGES CXX)
# Layer 1: Low-level utility
add_library(platform src/platform.cpp)
target_include_directories(platform PUBLIC include/platform)
target_compile_definitions(platform PUBLIC PLATFORM_LINUX)
# Layer 2: Networking uses platform
add_library(networking src/networking.cpp)
target_include_directories(networking PUBLIC include/networking)
target_link_libraries(networking
PUBLIC platform # Networking's headers expose platform types
)
# Layer 3: HTTP uses networking
add_library(http_client src/http_client.cpp)
target_include_directories(http_client PUBLIC include/http)
target_link_libraries(http_client
PUBLIC networking # HTTP headers expose networking types
)
# Final executable
add_executable(web_app src/main.cpp)
target_link_libraries(web_app PRIVATE http_client)
# web_app transitively gets:
# include/http, include/networking, include/platform
# PLATFORM_LINUX definition
# Links against: http_client, networking, platform
The propagation follows clear rules based on how you link:
| A links B as... | B's PUBLIC reqs go to A's... | B's INTERFACE reqs go to A's... |
|---|---|---|
PUBLIC | PUBLIC (propagates further) | PUBLIC (propagates further) |
PRIVATE | PRIVATE (stops here) | PRIVATE (stops here) |
INTERFACE | INTERFACE (propagates further) | INTERFACE (propagates further) |
Dependency Graph Example
flowchart TD
APP[web_app
Executable] -->|PRIVATE| HTTP[http_client
Shared Lib]
HTTP -->|PUBLIC| NET[networking
Static Lib]
HTTP -->|PRIVATE| JSON[json_parser
Static Lib]
NET -->|PUBLIC| PLAT[platform
Static Lib]
NET -->|PRIVATE| LOG[logger
Static Lib]
style APP fill:#BF092F,color:#fff
style HTTP fill:#16476A,color:#fff
style NET fill:#3B9797,color:#fff
style PLAT fill:#132440,color:#fff
style JSON fill:#666,color:#fff
style LOG fill:#666,color:#fff
In this graph, web_app transitively receives requirements from http_client → networking → platform (all PUBLIC links). It does not see json_parser or logger because those are linked PRIVATE at their respective levels.
Alias Targets
An alias target is a read-only reference to another target, typically using a namespace prefix. They're documented under add_library(ALIAS).
Namespacing Convention
cmake_minimum_required(VERSION 3.21)
project(AliasDemo LANGUAGES CXX)
# Create the real library target
add_library(myproject_core src/core.cpp)
target_include_directories(myproject_core PUBLIC include/)
# Create a namespaced alias
add_library(MyProject::Core ALIAS myproject_core)
# Now consumers can use the namespaced name
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE MyProject::Core)
Why use aliases with :: in the name? Two critical benefits:
- Error detection — If you typo
target_link_libraries(app PRIVATE MyProject::Croe), CMake immediately errors with "Target MyProject::Croe not found." Without the alias, a plain name likemyproject_croewould silently become a linker flag-lmyproject_croeand fail much later. - find_package compatibility — The same consumer code works whether the library is a subdirectory or an installed package.
find_package Compatibility
# In consumer's CMakeLists.txt — works both ways:
# 1. If MyProject is added via add_subdirectory()
# 2. If MyProject is installed and found via find_package(MyProject)
target_link_libraries(myapp PRIVATE MyProject::Core)
# Because:
# - add_subdirectory() exposes MyProject::Core as an ALIAS
# - find_package(MyProject) creates MyProject::Core as an IMPORTED target
# Both use the same MyProject::Core name!
Namespace::Target naming for every library you define. This makes your project usable both as a subdirectory (via add_subdirectory) and as an installed package (via find_package) with zero consumer code changes.
Imported Targets
An imported target represents a pre-built library or executable that exists outside your project. They're the mechanism by which find_package() exposes external dependencies. See add_library(IMPORTED).
Creating Imported Targets Manually
cmake_minimum_required(VERSION 3.21)
project(ImportedDemo LANGUAGES CXX)
# Create an imported shared library target
add_library(external_math SHARED IMPORTED)
# Set its location (where the .so/.dll lives)
set_target_properties(external_math PROPERTIES
IMPORTED_LOCATION "/usr/local/lib/libextmath.so"
INTERFACE_INCLUDE_DIRECTORIES "/usr/local/include/extmath"
INTERFACE_COMPILE_DEFINITIONS "EXTMATH_SHARED"
)
# Now use it like any other target
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE external_math)
# myapp gets:
# -I/usr/local/include/extmath
# -DEXTMATH_SHARED
# -lextmath
How find_package Creates Imported Targets
When you call find_package(ZLIB), the FindZLIB.cmake module (or zlib's own ZLIBConfig.cmake) creates imported targets behind the scenes:
cmake_minimum_required(VERSION 3.21)
project(FindPackageDemo LANGUAGES CXX)
# find_package creates ZLIB::ZLIB as an IMPORTED target
find_package(ZLIB REQUIRED)
# Inspect what find_package created
get_target_property(zlib_loc ZLIB::ZLIB IMPORTED_LOCATION)
get_target_property(zlib_inc ZLIB::ZLIB INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "ZLIB location: ${zlib_loc}")
message(STATUS "ZLIB includes: ${zlib_inc}")
# Use it — all requirements propagate automatically
add_executable(compressor src/main.cpp)
target_link_libraries(compressor PRIVATE ZLIB::ZLIB)
Inspect What find_package Creates
Add these lines after any find_package() call to see exactly what properties the imported target carries:
cmake_minimum_required(VERSION 3.21)
project(InspectImported LANGUAGES CXX)
find_package(Threads REQUIRED)
# Print all properties of the imported target
get_target_property(type Threads::Threads TYPE)
get_target_property(loc Threads::Threads INTERFACE_LINK_LIBRARIES)
get_target_property(opts Threads::Threads INTERFACE_COMPILE_OPTIONS)
message(STATUS "Threads type: ${type}")
message(STATUS "Threads link: ${loc}")
message(STATUS "Threads opts: ${opts}")
Run cmake -S . -B build and inspect the output. The Threads imported target typically provides -pthread on Linux systems.
Custom Targets
A custom target runs arbitrary commands but doesn't produce a known output file. It's useful for tasks like "run clang-format", "generate docs", or "deploy artifacts". See add_custom_target().
add_custom_target Basics
cmake_minimum_required(VERSION 3.21)
project(CustomTargetDemo LANGUAGES CXX)
add_executable(myapp src/main.cpp)
# Custom target: format all source files
add_custom_target(format
COMMAND clang-format -i ${PROJECT_SOURCE_DIR}/src/*.cpp
COMMAND clang-format -i ${PROJECT_SOURCE_DIR}/include/*.h
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMENT "Running clang-format on all sources..."
VERBATIM
)
# Custom target: generate documentation
find_package(Doxygen)
if(DOXYGEN_FOUND)
add_custom_target(docs
COMMAND ${DOXYGEN_EXECUTABLE} ${PROJECT_SOURCE_DIR}/Doxyfile
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMENT "Generating API documentation with Doxygen..."
VERBATIM
)
endif()
# Custom target: run static analysis
add_custom_target(analyze
COMMAND cppcheck --enable=all --std=c++20
${PROJECT_SOURCE_DIR}/src/
COMMENT "Running cppcheck static analysis..."
VERBATIM
)
Build custom targets explicitly:
# Custom targets are NOT built by default (unless ALL keyword is used)
cmake --build build --target format
cmake --build build --target docs
cmake --build build --target analyze
DEPENDS Keyword
Use DEPENDS to establish ordering between custom targets and other targets:
cmake_minimum_required(VERSION 3.21)
project(DependsDemo LANGUAGES CXX)
add_executable(myapp src/main.cpp)
# Custom target that depends on myapp being built first
add_custom_target(run
COMMAND $<TARGET_FILE:myapp> --verbose
DEPENDS myapp
WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
COMMENT "Running myapp..."
VERBATIM
)
# Now: cmake --build build --target run
# This automatically builds myapp first, then runs it
Custom Commands
Unlike custom targets, custom commands produce specific output files and integrate into the dependency graph. CMake knows when to re-run them based on whether their outputs are missing or their inputs changed. See add_custom_command().
OUTPUT Form — Generating Files at Build Time
cmake_minimum_required(VERSION 3.21)
project(CustomCommandOutput LANGUAGES CXX)
# Generate a version header at build time
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/generated/version.h
COMMAND ${CMAKE_COMMAND} -E echo
"\#define APP_VERSION \"1.2.3\"" > ${CMAKE_BINARY_DIR}/generated/version.h
COMMAND ${CMAKE_COMMAND} -E echo
"\#define BUILD_DATE \"${CMAKE_CURRENT_DATE}\"" >> ${CMAKE_BINARY_DIR}/generated/version.h
COMMENT "Generating version.h..."
VERBATIM
)
# The generated file must be listed as a source or dependency
add_executable(myapp
src/main.cpp
${CMAKE_BINARY_DIR}/generated/version.h # CMake sees this and runs the command
)
target_include_directories(myapp PRIVATE ${CMAKE_BINARY_DIR}/generated)
A more realistic example — using a code generator:
cmake_minimum_required(VERSION 3.21)
project(ProtobufExample LANGUAGES CXX)
# Custom command: compile .proto to .pb.cc and .pb.h
set(PROTO_SRC ${CMAKE_BINARY_DIR}/messages.pb.cc)
set(PROTO_HDR ${CMAKE_BINARY_DIR}/messages.pb.h)
add_custom_command(
OUTPUT ${PROTO_SRC} ${PROTO_HDR}
COMMAND protoc
--cpp_out=${CMAKE_BINARY_DIR}
--proto_path=${PROJECT_SOURCE_DIR}/proto
${PROJECT_SOURCE_DIR}/proto/messages.proto
DEPENDS ${PROJECT_SOURCE_DIR}/proto/messages.proto
COMMENT "Compiling messages.proto..."
VERBATIM
)
add_executable(server src/server.cpp ${PROTO_SRC})
target_include_directories(server PRIVATE ${CMAKE_BINARY_DIR})
add_custom_command only runs when something in the build depends on its output files. If no target lists the output as a source or dependency, the command will never execute. Always ensure generated files are consumed by a target.
TARGET Form — Post/Pre Build Steps
The TARGET form attaches commands to an existing target's build lifecycle:
cmake_minimum_required(VERSION 3.21)
project(TargetFormDemo LANGUAGES CXX)
add_executable(myapp src/main.cpp)
# Run AFTER myapp is built
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:myapp>
${PROJECT_SOURCE_DIR}/deploy/
COMMENT "Copying myapp to deploy directory..."
VERBATIM
)
# Run BEFORE myapp is linked
add_custom_command(TARGET myapp PRE_LINK
COMMAND ${CMAKE_COMMAND} -E echo "About to link myapp..."
VERBATIM
)
# Run BEFORE myapp compilation starts
add_custom_command(TARGET myapp PRE_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Starting myapp build..."
VERBATIM
)
Target Sources
The target_sources() command lets you add source files to a target after its initial creation. This is particularly useful for organizing large targets across multiple directories.
Adding Sources Incrementally
cmake_minimum_required(VERSION 3.21)
project(TargetSourcesDemo LANGUAGES CXX)
# Create target with minimal sources
add_library(engine)
# Add sources from different subdirectories
target_sources(engine PRIVATE
src/engine/core.cpp
src/engine/renderer.cpp
src/engine/physics.cpp
)
target_sources(engine PRIVATE
src/engine/audio/mixer.cpp
src/engine/audio/decoder.cpp
)
target_sources(engine PRIVATE
src/engine/input/keyboard.cpp
src/engine/input/gamepad.cpp
)
target_include_directories(engine PUBLIC include/)
This pattern works well with add_subdirectory() — each subdirectory's CMakeLists.txt can add its own sources to the parent target:
# src/engine/audio/CMakeLists.txt
target_sources(engine PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/mixer.cpp
${CMAKE_CURRENT_SOURCE_DIR}/decoder.cpp
${CMAKE_CURRENT_SOURCE_DIR}/effects.cpp
)
FILE_SET for Headers (CMake 3.23+)
CMake 3.23 introduced FILE_SET — a modern way to declare header files that replaces manual target_include_directories for installed libraries:
cmake_minimum_required(VERSION 3.23)
project(FileSetDemo LANGUAGES CXX)
add_library(mylib src/mylib.cpp)
# Declare public headers as a FILE_SET
target_sources(mylib
PUBLIC FILE_SET HEADERS
BASE_DIRS include
FILES
include/mylib/api.h
include/mylib/types.h
include/mylib/config.h
)
# Private implementation headers
target_sources(mylib
PRIVATE FILE_SET private_headers TYPE HEADERS
BASE_DIRS src
FILES
src/detail/impl.h
src/detail/helpers.h
)
# Install: FILE_SET headers are automatically installed correctly!
install(TARGETS mylib
FILE_SET HEADERS DESTINATION include
)
target_include_directories with different paths for build vs install (BUILD_INTERFACE vs INSTALL_INTERFACE generator expressions). FILE_SET handles this automatically — CMake knows exactly which headers belong to the target and where they should be installed.
Interface Libraries as Build Requirements
An interface library has no source files and produces no build artifact. Its sole purpose is to bundle and propagate build requirements. This makes it perfect for project-wide settings, compiler warning configurations, and shared requirements.
Creating Requirement Bundles
cmake_minimum_required(VERSION 3.21)
project(InterfaceLibDemo LANGUAGES CXX)
# Bundle: Project-wide C++ standard and common settings
add_library(project_defaults INTERFACE)
target_compile_features(project_defaults INTERFACE cxx_std_20)
target_compile_definitions(project_defaults INTERFACE
$<$<CONFIG:Debug>:DEBUG_MODE=1>
$<$<CONFIG:Release>:NDEBUG>
PROJECT_NAME="${PROJECT_NAME}"
PROJECT_VERSION="${PROJECT_VERSION}"
)
# Every target in the project links against project_defaults
add_library(core src/core.cpp)
target_link_libraries(core PUBLIC project_defaults)
add_library(utils src/utils.cpp)
target_link_libraries(utils PUBLIC project_defaults)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE core utils)
# myapp gets C++20, debug/release definitions from project_defaults
Compiler Warning Targets
A common pattern is creating an interface library that encapsulates your project's warning policy:
cmake_minimum_required(VERSION 3.21)
project(WarningTarget LANGUAGES CXX)
# Create a "warnings" interface target
add_library(project_warnings INTERFACE)
# Platform-specific warning flags
target_compile_options(project_warnings INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:
-Wall -Wextra -Wpedantic
-Wshadow -Wnon-virtual-dtor
-Wold-style-cast -Wcast-align
-Wunused -Woverloaded-virtual
-Wconversion -Wsign-conversion
-Wnull-dereference
-Wformat=2
>
$<$<CXX_COMPILER_ID:MSVC>:
/W4 /w14242 /w14254 /w14263
/w14265 /w14287 /w14296
/w14311 /w14545 /w14546
/w14547 /w14549 /w14555
/w14619 /w14640 /w14826
/w14905 /w14906 /w14928
/permissive-
>
)
# Optional: even stricter warnings as errors
add_library(project_warnings_strict INTERFACE)
target_link_libraries(project_warnings_strict INTERFACE project_warnings)
target_compile_options(project_warnings_strict INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Werror>
$<$<CXX_COMPILER_ID:MSVC>:/WX>
)
# Usage: Apply warnings to your targets
add_library(mylib src/mylib.cpp)
target_link_libraries(mylib PRIVATE project_warnings)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE project_warnings_strict)
Multi-Layer Interface Target Setup
Create a project with these interface targets forming a clean layered architecture:
cmake_minimum_required(VERSION 3.21)
project(LayeredArchitecture VERSION 1.0.0 LANGUAGES CXX)
# Layer 1: Compiler standard
add_library(std_cxx20 INTERFACE)
target_compile_features(std_cxx20 INTERFACE cxx_std_20)
# Layer 2: Warning policy (depends on standard)
add_library(warnings INTERFACE)
target_link_libraries(warnings INTERFACE std_cxx20)
target_compile_options(warnings INTERFACE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
# Layer 3: Sanitizers for debug builds
add_library(sanitizers INTERFACE)
target_compile_options(sanitizers INTERFACE
$<$<AND:$<CONFIG:Debug>,$<CXX_COMPILER_ID:GNU,Clang>>:
-fsanitize=address,undefined -fno-omit-frame-pointer>
)
target_link_options(sanitizers INTERFACE
$<$<AND:$<CONFIG:Debug>,$<CXX_COMPILER_ID:GNU,Clang>>:
-fsanitize=address,undefined>
)
# Combined: project-wide defaults
add_library(project_options INTERFACE)
target_link_libraries(project_options INTERFACE warnings sanitizers)
# All targets use project_options
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE project_options)
Try building in Debug mode with GCC/Clang to see sanitizer output. Remove sanitizers from project_options for Release builds.
Another powerful pattern — packaging platform-specific threading requirements:
cmake_minimum_required(VERSION 3.21)
project(ThreadBundle LANGUAGES CXX)
# Create an interface target that bundles threading requirements
add_library(threading INTERFACE)
find_package(Threads REQUIRED)
target_link_libraries(threading INTERFACE Threads::Threads)
target_compile_definitions(threading INTERFACE ENABLE_THREADING)
# Platform-specific threading extras
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(threading INTERFACE rt) # POSIX realtime
endif()
# Any target that needs threading just links to our bundle
add_library(task_system src/task_system.cpp)
target_link_libraries(task_system PUBLIC threading)
add_executable(worker src/main.cpp)
target_link_libraries(worker PRIVATE task_system)
Exercises
PUBLIC vs PRIVATE Classification
Given a library image_loader that uses libpng internally but exposes its own Image struct to consumers:
- Should
target_link_libraries(image_loader ... PNG::PNG)be PUBLIC or PRIVATE? - If
image_loader.hincludes<png.h>in its public header, does that change your answer? - Write the CMakeLists.txt for both scenarios and test that consumers compile correctly.
Hint: If a dependency's types appear in your public headers, consumers need to see it → PUBLIC. If you wrap everything behind your own types → PRIVATE.
Build-Time File Generation
Create a project that:
- Has a Python script (
generate_config.py) that outputs a C++ header with build timestamp and git hash - Uses
add_custom_command(OUTPUT ...)to run the script at build time - Ensures the command re-runs when the Python script changes (use DEPENDS)
- Includes the generated header in an executable
Verify that modifying the Python script triggers regeneration on the next build.
Project-Wide Configuration Target
Create an interface library called project_config that bundles:
- C++20 standard requirement
- Platform-appropriate warning flags (GCC/Clang/MSVC)
- A
PROJECT_ROOTcompile definition pointing to the source directory - Address sanitizer in Debug builds (GCC/Clang only)
Create three targets (two libraries + one executable) that all link against project_config. Verify that changing one setting in project_config affects all three targets.
Conclusion & Next Steps
Targets are the backbone of modern CMake. Every build requirement — headers, flags, definitions, linked libraries — belongs to a target with explicit visibility. This article covered:
- Target-centric philosophy — Targets replace global variables as the unit of abstraction
- Properties —
set_target_propertiesfor metadata,target_*commands for propagating requirements - Visibility — PRIVATE (build-only), PUBLIC (build + propagate), INTERFACE (propagate-only)
- Transitive dependencies — PUBLIC requirements cascade through the dependency graph
- Alias targets — Namespaced references with error detection and find_package compatibility
- Imported targets — Pre-built external artifacts created by find_package
- Custom targets/commands — Arbitrary tasks and build-time file generation
- target_sources & FILE_SET — Organizing large targets and modern header management
- Interface libraries — Requirement bundles for project-wide settings