Table of Contents

  1. Target-Centric Philosophy
  2. Target Properties
  3. PUBLIC vs PRIVATE vs INTERFACE
  4. Transitive Dependencies
  5. Alias Targets
  6. Imported Targets
  7. Custom Targets
  8. Custom Commands
  9. Target Sources
  10. Interface Libraries
  11. Exercises
  12. Conclusion & Next Steps
Back to CMake Mastery Series

Part 5: Working with Targets

June 4, 2026 Wasil Zafar 45 min read

Modern CMake is built on targets — not variables. Learn target properties, visibility specifiers, transitive dependencies, alias and imported targets, custom commands, and interface libraries as build requirement bundles.

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.

Key Insight: A target is the fundamental unit of abstraction in CMake. Think of it like a class in object-oriented programming — it encapsulates its build requirements and exposes an interface to consumers. The old approach of setting global variables (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 TypeCreated ByOutput
Executableadd_executable()Binary program
Static Libraryadd_library(name STATIC ...).a / .lib
Shared Libraryadd_library(name SHARED ...).so / .dll / .dylib
Object Libraryadd_library(name OBJECT ...)Object files (no archive)
Interface Libraryadd_library(name INTERFACE)No output — pure requirements
Imported Targetadd_library(name IMPORTED ...)Pre-built external artifact
Alias Targetadd_library(ns::name ALIAS real)Reference to another target
Custom Targetadd_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}")
When to Use Which: Use 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.

PUBLIC / PRIVATE / INTERFACE Propagation
        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
Rule of Thumb: If a header you #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...
PUBLICPUBLIC (propagates further)PUBLIC (propagates further)
PRIVATEPRIVATE (stops here)PRIVATE (stops here)
INTERFACEINTERFACE (propagates further)INTERFACE (propagates further)

Dependency Graph Example

Transitive Dependency Graph
        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_clientnetworkingplatform (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:

  1. 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 like myproject_croe would silently become a linker flag -lmyproject_croe and fail much later.
  2. 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!
Best Practice: Always create aliases with 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)
Hands-On Exploring Imported Targets
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.

imported find_package inspection

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})
Critical: The OUTPUT form of 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
)
Why FILE_SET Matters: Before FILE_SET, you had to manually manage 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)
Hands-On Build a Complete Target Architecture
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.

interface-library layered-architecture sanitizers

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

Exercise 1 Visibility Practice
PUBLIC vs PRIVATE Classification

Given a library image_loader that uses libpng internally but exposes its own Image struct to consumers:

  1. Should target_link_libraries(image_loader ... PNG::PNG) be PUBLIC or PRIVATE?
  2. If image_loader.h includes <png.h> in its public header, does that change your answer?
  3. 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.

visibility dependencies
Exercise 2 Custom Command Code Generator
Build-Time File Generation

Create a project that:

  1. Has a Python script (generate_config.py) that outputs a C++ header with build timestamp and git hash
  2. Uses add_custom_command(OUTPUT ...) to run the script at build time
  3. Ensures the command re-runs when the Python script changes (use DEPENDS)
  4. Includes the generated header in an executable

Verify that modifying the Python script triggers regeneration on the next build.

custom-command codegen dependencies
Exercise 3 Interface Library Bundle
Project-Wide Configuration Target

Create an interface library called project_config that bundles:

  1. C++20 standard requirement
  2. Platform-appropriate warning flags (GCC/Clang/MSVC)
  3. A PROJECT_ROOT compile definition pointing to the source directory
  4. 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.

interface-library project-config generator-expressions

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
  • Propertiesset_target_properties for 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
Official Reference: Deep-dive into cmake-buildsystem(7) for the complete target model, and cmake-properties(7) for every available property.