CMake Advanced Features: Libraries, Variables, and Project Organization
In the previous CMake post, we covered the basics of installation and creating a simple “Hello World” project. Now it’s time to dive deeper into CMake’s real-world projects.
This post will cover variables, conditions, library management, project organization, and advanced build configurations that you’ll encounter in professional development.
Understanding CMakeLists.txt Structure
Project Declaration and Versioning
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Specify minimum CMake version
cmake_minimum_required(VERSION 3.16)
# Project with version and description
project(MyAdvancedProject
VERSION 1.2.3
DESCRIPTION "A comprehensive CMake example"
LANGUAGES CXX C)
# Set C++ standard
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
|
Variables in CMake
CMake provides several types of variables for different purposes:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # Set custom variables
set(SOURCE_DIR "${CMAKE_SOURCE_DIR}/src")
set(INCLUDE_DIR "${CMAKE_SOURCE_DIR}/include")
set(LIB_DIR "${CMAKE_SOURCE_DIR}/lib")
# Built-in variables (some examples)
message(STATUS "Source directory: ${CMAKE_SOURCE_DIR}")
message(STATUS "Binary directory: ${CMAKE_BINARY_DIR}")
message(STATUS "Current source dir: ${CMAKE_CURRENT_SOURCE_DIR}")
message(STATUS "System name: ${CMAKE_SYSTEM_NAME}")
message(STATUS "Compiler ID: ${CMAKE_CXX_COMPILER_ID}")
# Lists in CMake
set(SOURCE_FILES
src/main.cpp
src/math_utils.cpp
src/file_handler.cpp
src/network_client.cpp
)
set(HEADER_FILES
include/math_utils.h
include/file_handler.h
include/network_client.h
)
|
Conditional Logic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # Platform-specific settings
if(WIN32)
message(STATUS "Building for Windows")
set(PLATFORM_LIBS ws2_32)
elseif(APPLE)
message(STATUS "Building for macOS")
set(PLATFORM_LIBS "-framework Foundation")
elseif(UNIX)
message(STATUS "Building for Linux/Unix")
set(PLATFORM_LIBS pthread)
endif()
# Compiler-specific flags
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
set(COMPILER_FLAGS -Wall -Wextra -pedantic)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(COMPILER_FLAGS -Wall -Wextra -Wpedantic)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(COMPILER_FLAGS /W4)
endif()
# Build type configurations
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message(STATUS "Debug build configuration")
add_definitions(-DDEBUG_MODE)
elseif(CMAKE_BUILD_TYPE STREQUAL "Release")
message(STATUS "Release build configuration")
add_definitions(-DNDEBUG)
endif()
|
Working with Libraries
Creating Static Libraries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # Create a static library
add_library(mathlib STATIC
src/math_utils.cpp
src/statistics.cpp
src/geometry.cpp
)
# Set include directories for the library
target_include_directories(mathlib
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/internal
)
# Set compile features
target_compile_features(mathlib PUBLIC cxx_std_17)
# Set compile options
target_compile_options(mathlib PRIVATE ${COMPILER_FLAGS})
|
Creating Shared Libraries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| # Create a shared library
add_library(networklib SHARED
src/network_client.cpp
src/http_handler.cpp
src/json_parser.cpp
)
# Library properties
set_target_properties(networklib PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION ${PROJECT_VERSION_MAJOR}
PUBLIC_HEADER "include/network_client.h;include/http_handler.h"
)
target_include_directories(networklib
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
src
)
|
1
2
3
4
5
6
7
8
9
10
| # Header-only library (Interface library)
add_library(templatelib INTERFACE)
target_include_directories(templatelib
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(templatelib INTERFACE cxx_std_17)
|
Linking Libraries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Create executable
add_executable(main_app src/main.cpp)
# Link libraries to executable
target_link_libraries(main_app
PRIVATE
mathlib # Our static library
networklib # Our shared library
templatelib # Our header-only library
${PLATFORM_LIBS} # Platform-specific libraries
)
# Link system libraries
if(UNIX)
target_link_libraries(main_app PRIVATE m) # Math library on Unix
endif()
|
Advanced Project Organization
Multi-Directory Project Structure
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| advanced_project/
├── CMakeLists.txt # Root CMake file
├── cmake/
│ ├── FindCustomLib.cmake # Custom find modules
│ └── CompilerFlags.cmake # Custom configurations
├── src/
│ ├── CMakeLists.txt # Source CMake file
│ ├── main.cpp
│ ├── core/
│ │ ├── CMakeLists.txt
│ │ ├── engine.cpp
│ │ └── engine.h
│ └── utils/
│ ├── CMakeLists.txt
│ ├── math_utils.cpp
│ └── math_utils.h
├── tests/
│ ├── CMakeLists.txt # Test CMake file
│ ├── test_main.cpp
│ ├── test_engine.cpp
│ └── test_utils.cpp
├── docs/
│ └── CMakeLists.txt # Documentation
└── examples/
├── CMakeLists.txt # Examples
└── example1.cpp
|
Root CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
| cmake_minimum_required(VERSION 3.16)
project(AdvancedProject VERSION 1.0.0 LANGUAGES CXX)
# Set global properties
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Include custom modules
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(CompilerFlags)
# Options
option(BUILD_TESTS "Build test programs" ON)
option(BUILD_EXAMPLES "Build example programs" OFF)
option(BUILD_DOCS "Build documentation" OFF)
# Add subdirectories
add_subdirectory(src)
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
if(BUILD_EXAMPLES)
add_subdirectory(examples)
endif()
if(BUILD_DOCS)
add_subdirectory(docs)
endif()
|
src/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Core engine library
add_subdirectory(core)
add_subdirectory(utils)
# Main executable
add_executable(advanced_app main.cpp)
target_link_libraries(advanced_app
PRIVATE
core_engine
utils_lib
)
# Install targets
install(TARGETS advanced_app
RUNTIME DESTINATION bin
)
|
src/core/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| add_library(core_engine STATIC
engine.cpp
processor.cpp
manager.cpp
)
target_include_directories(core_engine
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
$<INSTALL_INTERFACE:include/core>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/internal
)
target_compile_features(core_engine PUBLIC cxx_std_17)
# Install headers
install(FILES engine.h processor.h manager.h
DESTINATION include/core
)
# Install library
install(TARGETS core_engine
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
|
Testing Integration
tests/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # Test executable
add_executable(unit_tests
test_main.cpp
test_engine.cpp
test_utils.cpp
)
target_link_libraries(unit_tests
PRIVATE
core_engine
utils_lib
)
# Custom test command
add_custom_target(run_tests
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
DEPENDS unit_tests
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
|
Build Configurations and Generator Expressions
Build Type Configurations
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| # Set default build type
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Choose build type" FORCE)
endif()
# Build type specific settings
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g -DNDEBUG")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")
# Generator expressions for conditional compilation
target_compile_definitions(main_app
PRIVATE
$<$<CONFIG:Debug>:DEBUG_BUILD>
$<$<CONFIG:Release>:RELEASE_BUILD>
$<$<BOOL:${WIN32}>:WINDOWS_BUILD>
$<$<BOOL:${UNIX}>:UNIX_BUILD>
)
# Conditional linking
target_link_libraries(main_app
PRIVATE
$<$<CONFIG:Debug>:debug_helpers>
$<$<PLATFORM_ID:Windows>:ws2_32>
$<$<PLATFORM_ID:Linux>:pthread>
)
|
Custom Build Configurations
1
2
3
4
5
6
7
8
9
10
11
12
| # Create custom build type
set(CMAKE_CXX_FLAGS_PROFILE
"-O2 -g -pg" CACHE STRING "Flags for profile builds"
)
set(CMAKE_EXE_LINKER_FLAGS_PROFILE
"-pg" CACHE STRING "Linker flags for profile builds"
)
mark_as_advanced(
CMAKE_CXX_FLAGS_PROFILE
CMAKE_EXE_LINKER_FLAGS_PROFILE
)
|
Installation and Packaging
Install Configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| # Include standard modules
include(GNUInstallDirs)
include(CMakePackageConfigHelpers)
# Install executable
install(TARGETS main_app
EXPORT AdvancedProjectTargets
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
# Install libraries with export
install(TARGETS core_engine utils_lib
EXPORT AdvancedProjectTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
# Install headers
install(DIRECTORY src/core/ src/utils/
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/advancedproject
FILES_MATCHING PATTERN "*.h"
)
# Create package config files
configure_package_config_file(
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/AdvancedProjectConfig.cmake.in"
"${CMAKE_CURRENT_BINARY_DIR}/AdvancedProjectConfig.cmake"
INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/AdvancedProject
)
write_basic_package_version_file(
"${CMAKE_CURRENT_BINARY_DIR}/AdvancedProjectConfigVersion.cmake"
VERSION ${PROJECT_VERSION}
COMPATIBILITY SameMajorVersion
)
# Install package config files
install(FILES
"${CMAKE_CURRENT_BINARY_DIR}/AdvancedProjectConfig.cmake"
"${CMAKE_CURRENT_BINARY_DIR}/AdvancedProjectConfigVersion.cmake"
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/AdvancedProject
)
# Install export targets
install(EXPORT AdvancedProjectTargets
FILE AdvancedProjectTargets.cmake
NAMESPACE AdvancedProject::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/AdvancedProject
)
|
Creating Packages
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| # CPack configuration
set(CPACK_PACKAGE_NAME "AdvancedProject")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Advanced CMake Project Example")
set(CPACK_PACKAGE_VENDOR "Your Company")
set(CPACK_PACKAGE_CONTACT "developer@yourcompany.com")
# Package generators
if(WIN32)
set(CPACK_GENERATOR "NSIS;ZIP")
elseif(APPLE)
set(CPACK_GENERATOR "DragNDrop;TGZ")
else()
set(CPACK_GENERATOR "DEB;RPM;TGZ")
endif()
# Debian package specific
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libstdc++6")
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Your Name <your.email@company.com>")
# RPM package specific
set(CPACK_RPM_PACKAGE_GROUP "Development/Tools")
set(CPACK_RPM_PACKAGE_LICENSE "MIT")
include(CPack)
|
Create cmake/linux-arm.cmake
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # Linux ARM cross-compilation toolchain
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# Cross-compiler settings
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
# Search paths
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
|
Use toolchain:
1
| cmake -DCMAKE_TOOLCHAIN_FILE=cmake/linux-arm.cmake -S . -B build-arm
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| # Platform-specific source files
if(WIN32)
list(APPEND PLATFORM_SOURCES src/platform/windows.cpp)
elseif(APPLE)
list(APPEND PLATFORM_SOURCES src/platform/macos.cpp)
elseif(UNIX)
list(APPEND PLATFORM_SOURCES src/platform/linux.cpp)
endif()
add_library(platform_lib ${PLATFORM_SOURCES})
# Platform-specific compiler flags
if(MSVC)
target_compile_options(platform_lib PRIVATE /W4 /WX)
else()
target_compile_options(platform_lib PRIVATE -Wall -Wextra -Werror)
endif()
|
Advanced CMake Techniques
Custom Commands and Targets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| # Generate version header
add_custom_command(
OUTPUT ${CMAKE_BINARY_DIR}/version.h
COMMAND ${CMAKE_COMMAND} -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
-DBINARY_DIR=${CMAKE_BINARY_DIR}
-DVERSION=${PROJECT_VERSION}
-P ${CMAKE_SOURCE_DIR}/cmake/GenerateVersion.cmake
DEPENDS ${CMAKE_SOURCE_DIR}/cmake/GenerateVersion.cmake
COMMENT "Generating version header"
)
# Custom target for code formatting
find_program(CLANG_FORMAT clang-format)
if(CLANG_FORMAT)
add_custom_target(format
COMMAND ${CLANG_FORMAT} -i ${SOURCE_FILES} ${HEADER_FILES}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "Formatting code with clang-format"
)
endif()
# Custom target for static analysis
find_program(CPPCHECK cppcheck)
if(CPPCHECK)
add_custom_target(analyze
COMMAND ${CPPCHECK} --enable=all --std=c++17 ${CMAKE_SOURCE_DIR}/src
COMMENT "Running static analysis with cppcheck"
)
endif()
|
Function and Macro Definitions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| # Custom function to add library with standard settings
function(add_project_library name)
cmake_parse_arguments(PARSE_ARGV 1 LIB
"STATIC;SHARED;INTERFACE" # Options
"" # Single-value args
"SOURCES;HEADERS;DEPS" # Multi-value args
)
# Determine library type
if(LIB_STATIC)
set(lib_type STATIC)
elseif(LIB_SHARED)
set(lib_type SHARED)
else()
set(lib_type INTERFACE)
endif()
# Create library
add_library(${name} ${lib_type} ${LIB_SOURCES})
# Set properties
if(NOT LIB_INTERFACE)
target_include_directories(${name}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
target_compile_features(${name} PUBLIC cxx_std_17)
endif()
# Link dependencies
if(LIB_DEPS)
target_link_libraries(${name} PRIVATE ${LIB_DEPS})
endif()
# Install
install(TARGETS ${name}
EXPORT ${PROJECT_NAME}Targets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
# Install headers
if(LIB_HEADERS)
install(FILES ${LIB_HEADERS}
DESTINATION include/${name}
)
endif()
endfunction()
# Usage
add_project_library(mylib
STATIC
SOURCES src/mylib.cpp
HEADERS include/mylib.h
DEPS Threads::Threads
)
|
Building and Usage Examples
Basic Build Commands
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Configure and build
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --parallel 4
# Install
cmake --install . --prefix /usr/local
# Run tests
ctest --output-on-failure
# Create package
cpack
|
Advanced Build Options
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # Debug build with tests
cmake -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -S . -B build-debug
cmake --build build-debug
# Release build with examples
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_EXAMPLES=ON -S . -B build-release
cmake --build build-release
# Cross-compilation
cmake -DCMAKE_TOOLCHAIN_FILE=cmake/arm-toolchain.cmake -S . -B build-arm
cmake --build build-arm
# Custom generator
cmake -G Ninja -S . -B build-ninja
ninja -C build-ninja
|
Integration with IDEs
1
2
3
4
5
6
7
8
| # Visual Studio
cmake -G "Visual Studio 16 2019" -A x64 -S . -B build-vs
# Xcode
cmake -G Xcode -S . -B build-xcode
# Qt Creator
cmake -DCMAKE_PREFIX_PATH=/path/to/qt -S . -B build-qt
|
Best Practices and Tips
1. Modern CMake Principles
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # Use target-based approach
target_link_libraries(myapp PRIVATE mylib)
target_include_directories(myapp PRIVATE include)
# Global commands
link_libraries(mylib)
include_directories(include)
# Use generator expressions
target_compile_definitions(myapp
PRIVATE $<$<CONFIG:Debug>:DEBUG_MODE>)
# Manual conditionals everywhere
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
add_definitions(-DDEBUG_MODE)
endif()
|
2. Project Organization
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Keep CMakeLists.txt files close to source
src/
├── CMakeLists.txt
├── core/
│ ├── CMakeLists.txt
│ └── core.cpp
└── utils/
├── CMakeLists.txt
└── utils.cpp
# Use meaningful target names
add_library(MyProject::Core ALIAS core_lib)
# Set properties on targets
set_target_properties(mylib PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)
|
3. Library Management
1
2
3
4
5
6
7
8
9
| # Link system libraries when needed
if(UNIX)
target_link_libraries(main_app PRIVATE pthread m)
endif()
# Create library with proper interface
add_library(mylib STATIC src/mylib.cpp)
target_include_directories(mylib PUBLIC include)
target_compile_features(mylib PUBLIC cxx_std_17)
|
Conclusion
This post covered the advanced features of CMake that make it powerful for real-world projects:
Key Takeaways:
Advanced Project Management:
- Variables, conditions, and generator expressions
- Multi-directory project organization
- Custom functions and macros
Library Management:
- Creating static, shared, and header-only libraries
- Proper library linking and interface design
- Modern target-based approach
Build System Integration:
- Cross-platform development and toolchains
- Multiple build configurations
- Testing and continuous integration
Deployment and Packaging:
- Installation rules and package configuration
- CPack for creating distributable packages
- Export/import mechanisms for library reuse