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_executableandadd_libraryto define build targets, andtarget_link_librariesandtarget_include_directoriesto specify dependencies. - Variables and Cache: Use
set()to define variables andoption()to provide user-configurable options.
Table of Contents
- Minimal CMakeLists.txt
- Targets: Executables and Libraries
- Finding External Libraries: find_package
- Build Types and Compiler Options
- Project Structure and Subdirectories
- Common Issues and Solutions
- Production Patterns
- Complete Example: Multi-Target Project
- Performance Optimization
- 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
| Command | Description |
|---|---|
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)
Practical Example: If mylib internally uses src/internal/impl.h, it should be added as PRIVATE. Headers exposed to external projects, like include/mylib/api.h, should be added as PUBLIC. When myapp links to mylib, it will only have access to the PUBLIC headers.
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
Recommended Layout
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_STANDARDandCMAKE_CXX_STANDARD_REQUIREDset explicitly
Targets
- Every include path set via
target_include_directories, not globalinclude_directories - Dependencies declared via
target_link_librarieswith correctPUBLIC/PRIVATE/INTERFACEscope - Warnings enabled (
-Wall -Wextraor/W4) at minimum in Debug builds
CI/Build Health
- Debug and Release build types both verified in CI
-
ctestwired 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
Related Articles (Internal Links)
Other articles related to this topic.
- C++ CMake Targets 완벽 가이드 | 타겟 기반 빌드 시스템
- C++ CMake find_package 완벽 가이드 | 외부 라이브러리 통합
- C++ Conan 완벽 가이드 | 현대적인 C++ 패키지 관리
- C++ vcpkg 완벽 가이드 | Microsoft C++ 패키지 관리자
Keywords Covered in This Article (Related Search Terms)
This article covers C++, cmake, build, makefile, tools, cross-platform.