From Global Variables to Target-Based CMake
Global include paths pollute builds; target-based CMake replaces them with target properties and PUBLIC PRIVATE INTERFACE visibility keywords.
22 Feb 2026, 07:05 UTC

The Problem: Global Include Pollution
In traditional CMake projects, include_directories() placed paths into a global scope, meaning every target subsequently created inherited those directories regardless of actual need. As projects scale, this obscures the true dependency graph and risks header collisions.
The Target-Based Thesis
Modern CMake (3.0+) treats each target as an object that declares its own requirements. target_include_directories(), target_compile_definitions(), and target_link_libraries() set include paths, compile definitions, and linked libraries per target.
PRIVATE: used only when building the target; consumers do not inherit.INTERFACE: not used to build the target, but required by anyone linking to it.PUBLIC: used for building the target and propagated to consumers.
Worked Example: Three-Layer Dependency Chain
add_library(core STATIC core.cpp)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
add_library(engine STATIC engine.cpp)
target_include_directories(engine PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_link_libraries(engine PUBLIC core)
executable(app main.cpp)
target_link_libraries(app PRIVATE engine)
Run cmake --version to confirm 3.0+; then cmake .. and inspect generated build files to verify include paths propagate correctly.
Trade-Off: PUBLIC Overuse
Marking every dependency as PUBLIC recreates global-like propagation, increasing recompilation when low-level headers change. Reserve PUBLIC for truly exposed APIs; use PRIVATE for internal implementation details.
Actionable Closing
Migrate bottom-up: convert leaf libraries first, then move up, finally remove all global include_directories() calls and resolve resulting build errors with explicit target links.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.