Stop Repeating Include Paths: Encapsulate Usage with CMake Interface Libraries
Declare include paths, compile definitions and transitive links once in a CMake interface library and let linking propagate them. This removes duplication and keeps usage consistent across targets.
04 Jul 2025, 04:53 UTC

Managing transitive include directories, compile definitions and linked dependencies across many targets quickly becomes duplication. You add the same include path to three executables, forget one, and get a build that works on your machine but fails downstream. The useful takeaway is to declare those usage requirements once in a CMake interface library and let linking propagate them automatically.
The problem is repeated usage requirements
Header‑only code, compiler flags and third‑party headers are not linked as objects, but consumers still need include paths, definitions and transitive libraries. Copy‑pasting target_include_directories and target_compile_definitions creates drift. When a path changes you have to hunt every target.
An interface library is a CMake target with no sources. It exists only to carry usage requirements. Any target that links to it inherits those requirements with the visibility you choose.
What an interface library propagates
Interface properties are set with INTERFACE scope and are used by dependents, not by the library itself.
target_include_directories(... INTERFACE ...)target_compile_definitions(... INTERFACE ...)target_link_libraries(... INTERFACE ...)target_compile_options(... INTERFACE ...)
Because the library has no sources, CMake never tries to build or link it. There is no -lMyUtils in the link line, only the propagated usage.
Define and consume an interface library
Define it once in the library’s CMakeLists.txt. Run from the project root with write permission to the build directory.
# utils/CMakeLists.txt
add_library(MyUtils INTERFACE)
target_include_directories(MyUtils INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_definitions(MyUtils INTERFACE
MY_UTIL_ENABLE
)
# optional transitive dependency
# target_link_libraries(MyUtils INTERFACE some::other)
Consume it in an executable or another library:
# app/CMakeLists.txt
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE MyUtils)
PRIVATE means the requirements are used for myapp but not re‑exported to targets that link to myapp. PUBLIC would re‑export, INTERFACE would only export.
Worked example: header‑only utils
Layout:
project/
utils/
include/utils.h
CMakeLists.txt
app/
main.cpp
CMakeLists.txt
CMakeLists.txt
utils/include/utils.h uses the definition:
#pragma once
#ifdef MY_UTIL_ENABLE
#define UTIL_TAG \"enabled\"
#else
#define UTIL_TAG \"disabled\"
#endif
app/main.cpp:
#include <utils.h>
int main(){ return 0; }
Top‑level CMakeLists.txt adds both subdirectories. Configure and build:
cmake -S . -B build
cmake --build build
To verify propagation, build verbosely:
cmake --build build -- VERBOSE=1
Expected checks in the compile command for main.cpp: an -I pointing to utils/include and a -D MY_UTIL_ENABLE. No -lMyUtils should appear in the link step because the target is interface‑only. Changing a property on MyUtils should cause dependents to rebuild automatically.
You can also inspect the target:
cmake -P - <<EOF
include(CMakePrintHelpers)
get_target_property(inc MyUtils INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS \"includes: ${inc}\")
EOF
Trade‑off and limitations
An interface library cannot provide object code. If you need compiled functions, use STATIC or SHARED and keep an interface target for pure usage requirements, or combine them with ALIAS targets.
Interface libraries were introduced in CMake 3.0. Projects that must support older CMake cannot use them.
Usage requirements that vary by configuration need generator expressions, e.g. $<$CONFIG:Debug>.... Otherwise the requirement applies to all configurations.
When installing or packaging, an interface library must be explicitly exported with install(TARGETS ...) and export(...). Otherwise downstream consumers will not receive its usage requirements.
Actionable closing
Refactor repeated include paths, definitions and transitive links into one interface library per logical usage set. Verify propagation with a verbose build and by inspecting target properties. For reusable packages, export the interface target so consumers get the same requirements without duplication.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.