본문으로 건너뛰기 C++ CMake Complete Guide | Mastering Cross-Platform Build...

C++ CMake Complete Guide | Mastering Cross-Platform Build Systems

C++ CMake Complete Guide | Mastering Cross-Platform Build Systems

이 글의 핵심

A comprehensive guide to building C++ projects with CMake for cross-platform compatibility. Learn about target configuration, library linking, integrating external packages, and build optimization.

originalId: cpp-cmake

What is CMake and Why Do You Need It?

The Pain of Cross-Platform Builds

Scenario: You’ve developed a C++ project using Visual Studio on Windows and now need to deploy it to a Linux server. On Windows, you build using .vcxproj files, but on Linux, you need to write a Makefile. If you also want to test on macOS, you’ll need to create an Xcode project. Managing build configurations for each platform separately can quickly turn into a maintenance nightmare. Solution: CMake allows you to write a platform-independent build configuration (CMakeLists.txt) that automatically generates platform-specific build systems (Makefile, Visual Studio projects, Ninja, etc.). Write it once, build it anywhere.

flowchart LR
    subgraph input[Input]
        cmake[CMakeLists.txt]
    end
    subgraph cmake_tool[CMake]
        gen[Generator]
    end
    subgraph output[Output]
        make["Makefile (Linux)"]
        vs["Visual Studio (Windows)"]
        xcode["Xcode (macOS)"]
        ninja["Ninja (All Platforms)"]
    end
    cmake --> gen
    gen --> make
    gen --> vs
    gen --> xcode
    gen --> ninja

Core Concepts of CMake

  • Build System Generator: CMake doesn’t build your project directly. Instead, it generates platform-specific build systems (e.g., Makefile, .sln).
  • Target-Based: Use add_executable and add_library to define build targets, and target_link_libraries and target_include_directories to specify dependencies.
  • Variables and Cache: Use set() to define variables and option() to provide user-configurable options.

Table of Contents

  1. Minimal CMakeLists.txt
  2. Targets: Executables and Libraries
  3. Finding External Libraries: find_package
  4. Build Types and Compiler Options
  5. Project Structure and Subdirectories
  6. Common Issues and Solutions
  7. Production Patterns
  8. Complete Example: Multi-Target Project
  9. Performance Optimization
  10. CMake Adoption Checklist

1. Minimal CMakeLists.txt

Building a Hello World

# CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyProject)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(myapp main.cpp)
# Create a build directory (out-of-source build)
mkdir build
cd build
# Run CMake (generate the build system)
cmake ..
# Build (compile using the generated build system)
cmake --build .
# Or directly use make (Linux/macOS)
make
# Run the executable
./myapp

Key Commands Explained

CommandDescription
cmake_minimum_required(VERSION 3.20)Specifies the minimum required version of CMake
project(MyProject)Sets the project name
set(CMAKE_CXX_STANDARD 20)Specifies the use of C++20 standard
add_executable(myapp main.cpp)Creates an executable target
cmake ..Generates the build system using the parent directory’s CMakeLists.txt
cmake --build .Platform-independent build command

2. Targets: Executables and Libraries

Executables

# Single source file
add_executable(myapp main.cpp)
# Multiple source files
add_executable(myapp
    src/main.cpp
    src/utils.cpp
    src/config.cpp
)
# Using variables
set(SOURCES
    src/main.cpp
    src/utils.cpp
)
add_executable(myapp ${SOURCES})

Libraries

# Static library (.a, .lib)
add_library(mylib STATIC
    src/lib.cpp
    src/helper.cpp
)
# Shared library (.so, .dll)
add_library(mylib SHARED
    src/lib.cpp
    src/helper.cpp
)
# Header-only library
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include)
# Link library to executable
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE mylib)

PUBLIC vs PRIVATE vs INTERFACE

# PRIVATE: Used only within the target
target_include_directories(mylib PRIVATE src/internal)
# PUBLIC: Used by the target and any target linking to it
target_include_directories(mylib PUBLIC include)
# INTERFACE: Used only by targets linking to this target (for header-only libraries)
target_include_directories(mylib INTERFACE include)

3. Finding External Libraries: find_package

Boost Example

find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE Boost::filesystem Boost::system)

OpenSSL Example

find_package(OpenSSL REQUIRED)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto)

Handling Missing Packages

find_package(SomeLib)
if(SomeLib_FOUND)
    target_link_libraries(myapp PRIVATE SomeLib::SomeLib)
else()
    message(WARNING "SomeLib not found, using fallback")
endif()

Using pkg-config

find_package(PkgConfig REQUIRED)
pkg_check_modules(CURL REQUIRED libcurl)
target_link_libraries(myapp PRIVATE ${CURL_LIBRARIES})
target_include_directories(myapp PRIVATE ${CURL_INCLUDE_DIRS})

4. Build Types and Compiler Options

Build Types

# Set default build type
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
# Flags for each build type
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -DDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "-O2 -g")
set(CMAKE_CXX_FLAGS_MINSIZEREL "-Os -DNDEBUG")
# Debug build
cmake -DCMAKE_BUILD_TYPE=Debug ..
# Release build
cmake -DCMAKE_BUILD_TYPE=Release ..
# Optimized build with debug info
cmake -DCMAKE_BUILD_TYPE=RelWithDebInfo ..

Target-Specific Compiler Options

add_executable(myapp main.cpp)
# Enable warnings
target_compile_options(myapp PRIVATE
    -Wall -Wextra -Wpedantic
    $<$<CONFIG:Debug>:-Werror>  # Treat warnings as errors in Debug mode
)
# Preprocessor definitions
target_compile_definitions(myapp PRIVATE
    APP_VERSION="1.0"
    $<$<CONFIG:Debug>:DEBUG_MODE>
)
# Include directories
target_include_directories(myapp PRIVATE
    ${CMAKE_SOURCE_DIR}/include
    ${CMAKE_BINARY_DIR}/generated
)

Generator Expressions

# Conditional flags based on build type
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-O0 -g>
    $<$<CONFIG:Release>:-O3>
)
# Conditional flags based on compiler
target_compile_options(myapp PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-fno-exceptions>
    $<$<CXX_COMPILER_ID:MSVC>:/EHsc>
)

5. Project Structure and Subdirectories

myproject/
├── CMakeLists.txt          # top-level project definition
├── src/
│   ├── CMakeLists.txt      # library/executable targets
│   ├── main.cpp
│   └── mylib.cpp
├── include/
│   └── mylib/
│       └── mylib.h
├── tests/
│   └── CMakeLists.txt      # test targets
└── build/                  # out-of-source build directory (not committed)

Top-Level CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_subdirectory(src)
enable_testing()
add_subdirectory(tests)

src/CMakeLists.txt

add_subdirectory lets each folder own its own target definitions instead of one sprawling top-level file — the pattern scales cleanly as the project grows.

add_library(mylib mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_SOURCE_DIR}/include)
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE mylib)

6. Common Issues and Solutions

”Could NOT find X” from find_package

CMake Error: Could NOT find OpenSSL (missing: OPENSSL_LIBRARIES OPENSSL_INCLUDE_DIR)

Fix: install the library’s dev package (e.g. apt install libssl-dev) or point CMake at a manual install with -DCMAKE_PREFIX_PATH=/path/to/openssl.

Stale Cache After Changing CMakeLists.txt

Editing CMakeLists.txt sometimes doesn’t take effect because CMake reuses CMakeCache.txt from a previous configure — deleting the build directory guarantees a clean reconfigure.

rm -rf build && mkdir build && cd build && cmake ..

Mixing include_directories() with target_include_directories()

The old global include_directories() leaks into every target in the directory, including unrelated ones — prefer target_include_directories() exclusively so include paths are scoped to the target that actually needs them.

# ❌ Avoid: leaks to every target below this line
include_directories(${CMAKE_SOURCE_DIR}/include)
# ✅ Prefer: scoped to myapp only
target_include_directories(myapp PRIVATE ${CMAKE_SOURCE_DIR}/include)

Linking Order Errors on Linux (Undefined Reference)

GCC/Clang’s linker resolves symbols left-to-right, so a library that depends on another must be listed before the one it depends on — target_link_libraries mostly handles this automatically via CMake’s dependency graph, but manual -l flags in CMAKE_EXE_LINKER_FLAGS do not.

# ✅ Let target_link_libraries resolve the order via the dependency graph
target_link_libraries(myapp PRIVATE mylib pthread)

7. Production Patterns

Options for Feature Toggles

option(BUILD_TESTS "Build test suite" ON)
option(ENABLE_SANITIZERS "Build with ASan/UBSan" OFF)
if(BUILD_TESTS)
    add_subdirectory(tests)
endif()
if(ENABLE_SANITIZERS)
    add_compile_options(-fsanitize=address,undefined)
    add_link_options(-fsanitize=address,undefined)
endif()

Exporting a Library for Downstream Consumers

A library meant to be consumed by other CMake projects (via find_package) needs an install/export step, not just a local build target.

install(TARGETS mylib EXPORT mylibTargets
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib
        RUNTIME DESTINATION bin)
install(DIRECTORY include/ DESTINATION include)
install(EXPORT mylibTargets
        FILE mylibConfig.cmake
        NAMESPACE mylib::
        DESTINATION lib/cmake/mylib)

Version-Gating CMake Features

Supporting a range of CMake versions across CI runners means gating newer features behind a version check instead of requiring the newest CMake unconditionally.

cmake_minimum_required(VERSION 3.20)
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.25")
    set_target_properties(myapp PROPERTIES UNITY_BUILD ON)  # newer feature
endif()

8. Complete Example: Multi-Target Project

A minimal but realistic multi-target project: a shared library, an executable that links it, and a test target — all wired together.

cmake_minimum_required(VERSION 3.20)
project(Calculator VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE)
    set(CMAKE_BUILD_TYPE Release)
endif()
# Library
add_library(calc_lib src/calculator.cpp)
target_include_directories(calc_lib PUBLIC ${CMAKE_SOURCE_DIR}/include)
target_compile_options(calc_lib PRIVATE -Wall -Wextra)
# Executable
add_executable(calc_app src/main.cpp)
target_link_libraries(calc_app PRIVATE calc_lib)
# Tests
enable_testing()
find_package(GTest QUIET)
if(GTest_FOUND)
    add_executable(calc_tests tests/calculator_test.cpp)
    target_link_libraries(calc_tests PRIVATE calc_lib GTest::gtest_main)
    add_test(NAME calc_tests COMMAND calc_tests)
endif()
mkdir build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --parallel
ctest --output-on-failure

9. Performance Optimization

Build Speed: Parallel Compilation

# Use all available cores
cmake --build . --parallel $(nproc)     # Linux
cmake --build . --parallel $(sysctl -n hw.ncpu)  # macOS

Build Speed: ccache

Caching object files from previous compiles avoids recompiling unchanged translation units, which matters most on CI where the same headers get compiled repeatedly across branches.

find_program(CCACHE_PROGRAM ccache)
if(CCACHE_PROGRAM)
    set(CMAKE_CXX_COMPILER_LAUNCHER ${CCACHE_PROGRAM})
endif()

Build Speed: Precompiled Headers

target_precompile_headers(myapp PRIVATE
    <vector>
    <string>
    <memory>
)

Runtime Performance: LTO

Link-Time Optimization lets the compiler optimize across translation-unit boundaries — most valuable for release builds where build time is a lesser concern than runtime speed.

include(CheckIPOSupported)
check_ipo_supported(RESULT ipo_supported)
if(ipo_supported)
    set_target_properties(myapp PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE)
endif()

10. CMake Adoption Checklist

Project Setup

  • cmake_minimum_required() pinned to the actual minimum tested version
  • Out-of-source build directory (build/) added to .gitignore
  • CMAKE_CXX_STANDARD and CMAKE_CXX_STANDARD_REQUIRED set explicitly

Targets

  • Every include path set via target_include_directories, not global include_directories
  • Dependencies declared via target_link_libraries with correct PUBLIC/PRIVATE/INTERFACE scope
  • Warnings enabled (-Wall -Wextra or /W4) at minimum in Debug builds

CI/Build Health

  • Debug and Release build types both verified in CI
  • ctest wired into the test target and run in CI
  • Build cache (ccache or equivalent) enabled for CI speed

Distribution (if applicable)

  • install() rules defined for any target meant to be installed
  • find_package() config exported for downstream CMake consumers

Other articles related to this topic.



Keywords Covered in This Article (Related Search Terms)

This article covers C++, cmake, build, makefile, tools, cross-platform.