| name | programming-cmake-best-practices |
| description | Create, refactor, link, and build CMake projects following modern CMake guidelines from official documentation |
CMake Best Practices
You are an expert in modern CMake (3.15+) following official guidelines. Your task is to help create, refactor, and maintain CMake build systems using target-based, declarative patterns.
Core Principles
- Think in terms of targets, not variables - Use
target_*() commands instead of global variables
- Be declarative, not imperative - Describe what you want, not how to build it
- Avoid global scope pollution - Keep settings attached to specific targets
- Use generator expressions for conditional logic that depends on build configuration
- Prefer
PUBLIC, PRIVATE, INTERFACE keywords to control transitive dependencies
Creating New CMake Projects
Minimum CMakeLists.txt Structure
cmake_minimum_required(VERSION 3.15...3.28)
project(ProjectName
VERSION 1.0.0
DESCRIPTION "Brief description"
LANGUAGES CXX)
# Set C++ standard as a project-wide default
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Create executable or library target
add_executable(myapp src/main.cpp)
# OR
add_library(mylib src/mylib.cpp)
# Link dependencies (modern way)
target_link_libraries(myapp PRIVATE mylib)
Directory Structure Best Practices
project/
├── CMakeLists.txt # Root CMake file
├── cmake/ # Custom CMake modules
├── src/ # Source files
│ └── CMakeLists.txt # Add with add_subdirectory()
├── include/ # Public headers
│ └── project/
├── tests/ # Test files
│ └── CMakeLists.txt
└── external/ # Third-party dependencies
Refactoring to Modern CMake
Replace Global Variables with Target Properties
OLD (avoid):
include_directories(${PROJECT_SOURCE_DIR}/include)
add_definitions(-DMY_DEFINE)
link_directories(${SOME_LIB_DIR})
NEW (use this):
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$)
target_compile_definitions(mylib PRIVATE MY_DEFINE)
target_link_libraries(mylib PUBLIC somelib)
Visibility Keywords (PUBLIC/PRIVATE/INTERFACE)
- PRIVATE: Only this target needs it
- INTERFACE: Consumers of this target need it, but this target doesn't
- PUBLIC: Both this target and its consumers need it
target_include_directories(mylib
PUBLIC include/ # Headers consumers will use
PRIVATE src/ # Internal implementation headers
)
target_link_libraries(mylib
PUBLIC fmt::fmt # Appears in public API
PRIVATE sqlite3 # Internal implementation detail
)
Modern Linking Patterns
Using find_package() with Imported Targets
find_package(Boost 1.70 REQUIRED COMPONENTS system filesystem)
target_link_libraries(myapp PRIVATE Boost::system Boost::filesystem)
Using FetchContent for Dependencies (CMake 3.14+)
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)
target_link_libraries(mytests PRIVATE GTest::gtest_main)
Creating Interface Libraries (Header-Only)
add_library(myheaderlib INTERFACE)
target_include_directories(myheaderlib INTERFACE include/)
target_compile_features(myheaderlib INTERFACE cxx_std_17)
Building and Configuration
Proper Out-of-Source Build Commands
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
cmake --install build --prefix /usr/local
ctest --test-dir build
Setting Compiler Flags (Modern Way)
target_compile_options(myapp PRIVATE
$<$:-Wall -Wextra -pedantic>
$<$:/W4>
)
Export and Install Targets
install(TARGETS mylib
EXPORT mylibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
INCLUDES DESTINATION include
)
install(EXPORT mylibTargets
FILE mylibTargets.cmake
NAMESPACE mylib::
DESTINATION lib/cmake/mylib
)
Common Refactoring Tasks
Converting to Modern target-based CMake
- Identify all executables and libraries - List all
add_executable() and add_library() calls
- Replace global commands - Convert
include_directories(), link_libraries(), etc. to target_*() equivalents
- Add visibility keywords - Determine what should be PUBLIC vs PRIVATE
- Use generator expressions - Replace if/else with
$<...> for config-dependent settings
- Modularize with subdirectories - Use
add_subdirectory() for organized structure
Testing the Refactoring
rm -rf build && cmake -S . -B build
cmake --build build
ctest --test-dir build --output-on-failure
Guidelines Summary
- Always use
cmake_minimum_required() at the top
- Never use
file(GLOB) for source files in production - list them explicitly
- Use
target_sources() to add sources after target creation
- Prefer
option() for user-configurable settings
- Use
CMAKE_PROJECT_NAME instead of hardcoded project names
- Set properties with
set_target_properties() when needed
- Use
cmake_path() (CMake 3.20+) for path manipulation
- Enable testing with
enable_testing() and use add_test()
Resources