Skip to content

⚙️ CMake Advanced Production

Kỹ thuật nâng cao để biến CMake project thành production-grade: dependency propagation, sanitizers, và static analysis.

PUBLIC vs PRIVATE vs INTERFACE — The Heart of Modern CMake

Đây là concept quan trọng nhất của Modern CMake. Hiểu sai = dependency hell.

Visualization

┌─────────────────────────────────────────────────────────────────────────┐
│           TARGET DEPENDENCY PROPAGATION                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   ┌───────────┐     links to      ┌───────────┐                         │
│   │   app     │ ───────────────► │   mylib   │                         │
│   └───────────┘                   └───────────┘                         │
│                                        │                                │
│                                        │ PUBLIC: spdlog                 │
│                                        │ PRIVATE: zlib                  │
│                                        │ INTERFACE: boost_headers       │
│                                        ▼                                │
│                                                                         │
│   Kết quả cho app:                                                      │
│   ├── GETS: spdlog (vì PUBLIC)                                          │
│   ├── GETS: boost_headers (vì INTERFACE)                                │
│   └── NOT GET: zlib (vì PRIVATE - implementation detail)               │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Định nghĩa chính xác

KeywordAi dùng?Use case
PUBLICLibrary + ConsumersDependency xuất hiện trong public headers
PRIVATEChỉ LibraryImplementation detail, không exposed
INTERFACEChỉ ConsumersHeader-only, hoặc export-only

Ví dụ thực tế

cmake
# Library definition
add_library(network STATIC
    src/connection.cpp
    src/socket.cpp
)

target_link_libraries(network
    # PUBLIC: consumer code cần include boost/asio.hpp
    PUBLIC Boost::asio
    
    # PRIVATE: OpenSSL dùng trong implementation, consumer không biết
    PRIVATE OpenSSL::SSL OpenSSL::Crypto
    
    # INTERFACE: N/A for this example
)

target_include_directories(network
    # PUBLIC: include/network/connection.hpp được consumer include
    PUBLIC ${CMAKE_SOURCE_DIR}/include/network
    
    # PRIVATE: src/internal/ chứa implementation headers
    PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/internal
)

# Consumer
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE network)
# app automatically gets: Boost::asio (PUBLIC từ network)
# app KHÔNG cần link: OpenSSL (PRIVATE của network)

Decision Flowchart

┌───────────────────────────────────────────────────────┐
│ Dependency X được dùng trong library L                │
└───────────────────────────────────────────────────────┘


        ┌───────────────────────────────┐
        │ X xuất hiện trong PUBLIC      │
        │ headers của L?                │
        └───────────────────────────────┘
                │               │
              YES             NO
                │               │
                ▼               ▼
        ┌───────────┐   ┌───────────────────────┐
        │  PUBLIC   │   │ X được dùng trong     │
        └───────────┘   │ implementation (cpp)? │
                        └───────────────────────┘
                                │           │
                              YES         NO
                                │           │
                                ▼           ▼
                        ┌───────────┐   ┌───────────┐
                        │  PRIVATE  │   │ INTERFACE │
                        └───────────┘   └───────────┘
                                        (header-only)

Static Analysis Integration

CMake Module: StaticAnalysis.cmake

Tạo file cmake/StaticAnalysis.cmake:

cmake
# cmake/StaticAnalysis.cmake
# Integration with clang-tidy and cppcheck

option(ENABLE_CLANG_TIDY "Enable clang-tidy static analysis" OFF)
option(ENABLE_CPPCHECK "Enable cppcheck static analysis" OFF)

# ============================================
# CLANG-TIDY
# ============================================
if(ENABLE_CLANG_TIDY)
    find_program(CLANG_TIDY_EXE NAMES clang-tidy clang-tidy-15 clang-tidy-14)
    
    if(CLANG_TIDY_EXE)
        message(STATUS "clang-tidy found: ${CLANG_TIDY_EXE}")
        set(CMAKE_CXX_CLANG_TIDY 
            ${CLANG_TIDY_EXE}
            -checks=*,-llvmlibc-*,-fuchsia-*,-google-runtime-references
            -warnings-as-errors=*
            --header-filter=${CMAKE_SOURCE_DIR}/include/.*
        )
    else()
        message(WARNING "clang-tidy requested but not found!")
    endif()
endif()

# ============================================
# CPPCHECK
# ============================================
if(ENABLE_CPPCHECK)
    find_program(CPPCHECK_EXE NAMES cppcheck)
    
    if(CPPCHECK_EXE)
        message(STATUS "cppcheck found: ${CPPCHECK_EXE}")
        set(CMAKE_CXX_CPPCHECK
            ${CPPCHECK_EXE}
            --enable=warning,performance,portability
            --suppress=missingIncludeSystem
            --inline-suppr
            --inconclusive
            --error-exitcode=1
        )
    else()
        message(WARNING "cppcheck requested but not found!")
    endif()
endif()

Sử dụng trong Root CMakeLists.txt

cmake
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)

# Include static analysis module
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
include(StaticAnalysis)

# Targets sau đây sẽ được scan
add_library(mylib src/lib.cpp)

Chạy Analysis

powershell
# Configure với static analysis enabled
cmake -B build -DENABLE_CLANG_TIDY=ON -DENABLE_CPPCHECK=ON

# Build (analysis chạy parallel với compile)
cmake --build build

Compiler Warnings Module

Tạo file cmake/CompilerWarnings.cmake:

cmake
# cmake/CompilerWarnings.cmake
# Strict warnings configuration

function(set_project_warnings target)
    set(CLANG_WARNINGS
        -Wall
        -Wextra
        -Wpedantic
        -Wshadow
        -Wunused
        -Wformat=2
        -Wcast-align
        -Wconversion
        -Wsign-conversion
        -Wnull-dereference
        -Wdouble-promotion
    )

    set(GCC_WARNINGS
        ${CLANG_WARNINGS}
        -Wmisleading-indentation
        -Wduplicated-cond
        -Wduplicated-branches
        -Wlogical-op
    )

    set(MSVC_WARNINGS
        /W4
        /permissive-
        /w14640   # thread un-safe static member initialization
        /w14242   # conversion, possible loss of data
        /w14254   # conversion, possible loss of data
        /w14263   # member function hides virtual function
        /w14265   # class has virtual functions but destructor is not virtual
        /w14287   # unsigned/negative constant mismatch
        /w14296   # expression is always true/false
    )

    if(MSVC)
        target_compile_options(${target} PRIVATE ${MSVC_WARNINGS})
    elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
        target_compile_options(${target} PRIVATE ${CLANG_WARNINGS})
    elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        target_compile_options(${target} PRIVATE ${GCC_WARNINGS})
    endif()
endfunction()

# Optional: Warnings as Errors for CI
option(WARNINGS_AS_ERRORS "Treat warnings as errors" OFF)

function(enable_warnings_as_errors target)
    if(WARNINGS_AS_ERRORS)
        if(MSVC)
            target_compile_options(${target} PRIVATE /WX)
        else()
            target_compile_options(${target} PRIVATE -Werror)
        endif()
    endif()
endfunction()

Usage

cmake
include(CompilerWarnings)

add_library(mylib src/lib.cpp)
set_project_warnings(mylib)
enable_warnings_as_errors(mylib)

🔴 CI/CD MANDATE

-Werror (hoặc /WX trên MSVC) BẮT BUỘC trong CI pipeline. Warnings = bugs waiting to happen.


Sanitizer Build Types

Tạo file cmake/Sanitizers.cmake:

cmake
# cmake/Sanitizers.cmake
# AddressSanitizer, UndefinedBehaviorSanitizer, ThreadSanitizer

option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
option(ENABLE_UBSAN "Enable UndefinedBehaviorSanitizer" OFF)
option(ENABLE_TSAN "Enable ThreadSanitizer" OFF)
option(ENABLE_MSAN "Enable MemorySanitizer (Clang only)" OFF)

function(enable_sanitizers target)
    if(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang" OR 
       CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        
        set(SANITIZER_FLAGS "")
        
        if(ENABLE_ASAN)
            list(APPEND SANITIZER_FLAGS -fsanitize=address)
            message(STATUS "AddressSanitizer ENABLED for ${target}")
        endif()
        
        if(ENABLE_UBSAN)
            list(APPEND SANITIZER_FLAGS -fsanitize=undefined)
            message(STATUS "UndefinedBehaviorSanitizer ENABLED for ${target}")
        endif()
        
        if(ENABLE_TSAN)
            if(ENABLE_ASAN)
                message(FATAL_ERROR "ASan and TSan cannot be used together!")
            endif()
            list(APPEND SANITIZER_FLAGS -fsanitize=thread)
            message(STATUS "ThreadSanitizer ENABLED for ${target}")
        endif()
        
        if(ENABLE_MSAN)
            if(NOT CMAKE_CXX_COMPILER_ID MATCHES ".*Clang")
                message(FATAL_ERROR "MemorySanitizer requires Clang!")
            endif()
            list(APPEND SANITIZER_FLAGS -fsanitize=memory)
            message(STATUS "MemorySanitizer ENABLED for ${target}")
        endif()
        
        if(SANITIZER_FLAGS)
            # Compile và Link flags
            target_compile_options(${target} PRIVATE ${SANITIZER_FLAGS} 
                                   -fno-omit-frame-pointer -g)
            target_link_options(${target} PRIVATE ${SANITIZER_FLAGS})
        endif()
        
    elseif(MSVC)
        if(ENABLE_ASAN)
            target_compile_options(${target} PRIVATE /fsanitize=address)
            message(STATUS "AddressSanitizer ENABLED for ${target} (MSVC)")
        endif()
        # Note: MSVC has limited sanitizer support
    endif()
endfunction()

Sanitizer Descriptions

┌─────────────────────────────────────────────────────────────────────────┐
│                     SANITIZER CHEAT SHEET                                │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│   🔍 AddressSanitizer (ASan)                                            │
│   ─────────────────────────                                             │
│   Detect: Buffer overflow, use-after-free, memory leaks                 │
│   Overhead: ~2x slowdown, ~2x memory                                    │
│   Essential for: Any code touching raw pointers                         │
│                                                                         │
│   🔍 UndefinedBehaviorSanitizer (UBSan)                                 │
│   ─────────────────────────────────────                                 │
│   Detect: Integer overflow, null pointer dereference, shift errors     │
│   Overhead: ~1.2x slowdown                                              │
│   Essential for: Numeric heavy code, parsing                            │
│                                                                         │
│   🔍 ThreadSanitizer (TSan)                                             │
│   ─────────────────────────                                             │
│   Detect: Data races, deadlocks                                         │
│   Overhead: ~5-15x slowdown, ~5-10x memory                              │
│   Essential for: Multithreaded code                                     │
│   ⚠️ Cannot combine with ASan                                           │
│                                                                         │
│   🔍 MemorySanitizer (MSan) - Clang only                                │
│   ─────────────────────────────────────                                 │
│   Detect: Uninitialized memory reads                                    │
│   Overhead: ~3x slowdown                                                │
│   Essential for: Security-critical code                                 │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

Usage

cmake
include(Sanitizers)

add_executable(app src/main.cpp)
enable_sanitizers(app)
powershell
# Configure với ASan + UBSan
cmake -B build -DENABLE_ASAN=ON -DENABLE_UBSAN=ON

# Build và run - sanitizers sẽ báo lỗi runtime
cmake --build build
./build/app

Example: ASan catches use-after-free

cpp
// bug.cpp - ASan sẽ catch lỗi này
#include <iostream>

int main() {
    int* ptr = new int(42);
    delete ptr;
    
    // Use-after-free - ASan báo lỗi:
    // ==12345==ERROR: AddressSanitizer: heap-use-after-free
    std::cout << *ptr << std::endl;  
    
    return 0;
}

Custom Build Types

cmake
# CMakeLists.txt
# Custom build type for sanitizer builds

# Define sanitizer configurations
set(CMAKE_CONFIGURATION_TYPES 
    "Debug;Release;RelWithDebInfo;ASan;TSan" 
    CACHE STRING "" FORCE
)

# ASan configuration
set(CMAKE_CXX_FLAGS_ASAN 
    "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fno-omit-frame-pointer"
    CACHE STRING "" FORCE
)
set(CMAKE_EXE_LINKER_FLAGS_ASAN
    "${CMAKE_EXE_LINKER_FLAGS_DEBUG} -fsanitize=address"
    CACHE STRING "" FORCE
)

# TSan configuration
set(CMAKE_CXX_FLAGS_TSAN 
    "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=thread -fno-omit-frame-pointer"
    CACHE STRING "" FORCE
)
set(CMAKE_EXE_LINKER_FLAGS_TSAN
    "${CMAKE_EXE_LINKER_FLAGS_DEBUG} -fsanitize=thread"
    CACHE STRING "" FORCE
)
powershell
# Build với specific configuration
cmake -B build -DCMAKE_BUILD_TYPE=ASan
cmake --build build

Custom make lint Target

cmake
# Custom target for running clang-tidy manually
find_program(CLANG_TIDY_EXE clang-tidy)

if(CLANG_TIDY_EXE)
    add_custom_target(lint
        COMMAND ${CLANG_TIDY_EXE}
            -p ${CMAKE_BINARY_DIR}
            --config-file=${CMAKE_SOURCE_DIR}/.clang-tidy
            ${CMAKE_SOURCE_DIR}/src/*.cpp
            ${CMAKE_SOURCE_DIR}/include/**/*.hpp
        COMMENT "Running clang-tidy static analysis..."
        VERBATIM
    )
endif()
powershell
# Run lint
cmake --build build --target lint

Export Compile Commands for IDE/LSP

cmake
# Always generate compile_commands.json
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

File compile_commands.json được tạo trong build/, cần cho:

  • clangd (VSCode C++ LSP)
  • clang-tidy standalone
  • IDE navigation (CLion, etc.)
powershell
# Symlink cho clangd
ln -s build/compile_commands.json compile_commands.json

Bước tiếp theo

Đã setup tool chain chuyên nghiệp, giờ học cách quản lý dependencies:

📦 Package Management → — Conan & Vcpkg: Stop reinventing the wheel