Post

CMake Advanced Features

CMake Advanced Features

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
)

Header-Only Libraries

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)

Cross-Platform Development

Toolchain Files

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

Platform-Specific Code

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
This post is licensed under CC BY 4.0 by the author.