Зачем CMake
C++ собирается не одинаково на разных платформах: у GCC, Clang, MSVC разные флаги и расширения библиотек. CMake — это генератор сборки: вы описываете проект на декларативном языке, а CMake производит Makefile, Ninja-файлы или проекты Visual Studio. Это стандарт де-факто для современных C++-проектов.
# типичный цикл
cmake -B build # генерация build-системы
cmake --build build # сборка
./build/myapp # запуск
Минимальный CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(myapp src/main.cpp)
Четыре строки — и у вас собираемый проект. add_executable создаёт таргет — конечную цель сборки. Всё в современном CMake крутится вокруг таргетов и их свойств.
Структура нормального проекта
myproject/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── core/
│ ├── engine.cpp
│ └── engine.h
└── tests/
└── CMakeLists.txt
Корневой файл задаёт проект и подключает подкаталоги:
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # для clangd/IDE
add_subdirectory(src)
add_subdirectory(tests)
А src/CMakeLists.txt описывает библиотеку и приложение:
add_library(core STATIC core/engine.cpp)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE core)
PRIVATE/PUBLIC/INTERFACE — это видимость зависимости:
- PUBLIC — зависимость нужна и этой цели, и тем, кто линкует её.
- PRIVATE — только этой цели.
- INTERFACE — только потребителям (для header-only библиотек).
Библиотеки: static, shared, header-only
add_library(core STATIC engine.cpp) # .a / .lib
add_library(core SHARED engine.cpp) # .so / .dll
add_library(core INTERFACE) # header-only, без сборки
target_include_directories(core INTERFACE include/) # для INTERFACE — без PRIVATE
Для header-only библиотеки весь контракт в INTERFACE-свойствах:
add_library(mylib INTERFACE)
target_include_directories(mylib INTERFACE include/)
target_compile_features(mylib INTERFACE cxx_std_20)
Зависимости: find_package и FetchContent
Если библиотека установлена в системе — find_package:
find_package(fmt REQUIRED) # ищет fmt на диске
target_link_libraries(myapp PRIVATE fmt::fmt)
Если нет — CMake умеет скачивать исходники с GitHub:
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)
Так подключаются почти все популярные библиотеки: fmt, spdlog, nlohmann_json, gtest — у всех есть CMake-таргеты.
Опции и конфигурации
option(ENABLE_TESTS "Build tests" ON)
option(ENABLE_SANITIZERS "ASan+UBSan" OFF)
if(ENABLE_SANITIZERS)
target_compile_options(myapp PRIVATE -fsanitize=address,undefined)
target_link_options(myapp PRIVATE -fsanitize=address,undefined)
endif()
Сборка в Debug/Release — через -DCMAKE_BUILD_TYPE:
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
Современный синтаксис: target_* вместо глобальных переменных
Старый стиль (include_directories(), add_definitions()) работает, но засоряет всё глобально. Современный CMake — только через таргеты:
target_include_directories(myapp PRIVATE src/)
target_compile_options(myapp PRIVATE -Wall -Wextra -Wpedantic)
target_compile_definitions(myapp PRIVATE DEBUG_MODE=1)
target_compile_features(myapp PRIVATE cxx_std_20)
target_link_libraries(myapp PRIVATE core fmt::fmt)
Тесты через CTest
enable_testing()
add_executable(tests test_engine.cpp)
target_link_libraries(tests PRIVATE core GTest::gtest_main)
add_test(NAME engine COMMAND tests)
ctest --test-dir build --output-on-failure
Типичные ошибки
CMAKE_CXX_STANDARDпослеadd_executable— переменные должны быть до таргетов.link_directoriesвместоtarget_link_libraries— лучше линковать таргеты напрямую.file(GLOB)для исходников — CMake не пересоберёт при добавлении файла; перечисляйте явно.- Отсутствие
cmake_minimum_required— непредсказуемое поведение на новых версиях. include_directoriesглобально — слипаются пути всех подпроектов.
Итог
Минимум для работы: add_executable/add_library + target_link_libraries + target_include_directories. Дальше — find_package для системных пакетов и FetchContent для гитовских зависимостей. С CMAKE_EXPORT_COMPILE_COMMANDS ваши IDE и clangd получат все пути. Один cmake -B build && cmake --build build — и проект собран на любой платформе.
Готовый шаблон проекта
cmake_minimum_required(VERSION 3.20)
project(App VERSION 1.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# зависимости
include(FetchContent)
FetchContent_Declare(fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 10.2.1)
FetchContent_MakeAvailable(fmt)
# библиотека ядра
add_library(core STATIC
src/engine/engine.cpp
src/util/log.cpp)
target_include_directories(core PUBLIC src/)
# приложение
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core fmt::fmt)
# тесты
option(BUILD_TESTS ON)
if(BUILD_TESTS)
enable_testing()
FetchContent_Declare(googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0)
FetchContent_MakeAvailable(googletest)
add_executable(tests tests/test_engine.cpp)
target_link_libraries(tests PRIVATE core GTest::gtest_main)
add_test(NAME tests COMMAND tests)
endif()
Полезные переменные и флаги
CMAKE_BUILD_TYPE # Debug / Release / RelWithDebInfo / MinSizeRel
CMAKE_CXX_COMPILER # выбрать компилятор явно
CMAKE_EXPORT_COMPILE_COMMANDS # compile_commands.json для clangd
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -- -j$(nproc) # параллельная сборка
cmake --build build --target app # только одна цель
Установка и упаковка
install(TARGETS app DESTINATION bin)
install(DIRECTORY assets/ DESTINATION share/app)
include(CPack) # deb/rpm/zip пакеты одной строкой
Что дальше
- Presets (
CMakePresets.json) — профили сборки для команды и CI. - Toolchain-файлы — кросс-компиляция на ARM/embedded.
ExternalProjectдля сборки чужих проектов без CMake.cmake --install, CPack, GitHub Actions — CI/CD для вашего проекта.
Хороший CMakeLists — это документация: по нему видно структуру, зависимости и опции проекта без чтения кода.