CMake не компилирует код сам: он читает CMakeLists.txt и генерирует файлы сборки для Make, Ninja или IDE. Такой слой кажется лишним в проекте из одного файла, но окупается, когда появляются библиотеки, тесты, зависимости и несколько платформ. Разберём рабочую структуру проекта, цели, presets и диагностику типичных ошибок.
Минимальный проект
Начните с отдельных каталогов src, include и build. Сборка вне исходников — не стилистическое пожелание, а способ не смешивать артефакты с кодом. Версию в cmake_minimum_required указывайте не ниже 3.20: этого достаточно для presets и современных генераторов.
cmake_minimum_required(VERSION 3.20)
project(app LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(app src/main.cpp)
target_include_directories(app PRIVATE include)
target_compile_options(app PRIVATE -Wall -Wextra)
CMAKE_EXPORT_COMPILE_COMMANDS создаёт compile_commands.json — его читают clangd, clang-tidy и редакторы. Флаги -Wall -Wextra лучше ставить через target_compile_options, а не глобально: так предупреждения не протекут в чужие цели.
Библиотеки и цели
Главная идея CMake — цель (target) со своими свойствами. Вместо глобальных include_directories и link_libraries задавайте зависимости у конкретной цели. Ключевые слова PRIVATE, PUBLIC и INTERFACE определяют, что увидят потребители библиотеки.
find_package(Threads REQUIRED)
add_library(core STATIC src/core.cpp)
target_include_directories(core PUBLIC include)
target_link_libraries(core PUBLIC Threads::Threads)
add_executable(app src/main.cpp)
target_link_libraries(app PRIVATE core)
Если заголовки core нужны только внутри библиотеки, используйте PRIVATE. Если они входят в публичный API — PUBLIC. INTERFACE пригодится для header-only библиотек: у них нет исходников, но есть требования к потребителю.
Зависимости
Системные библиотеки ищутся через find_package. Для небольших переносимых зависимостей удобен FetchContent: он скачивает исходники на этапе конфигурации и собирает их вместе с проектом. Второй способ требует сети, поэтому в офлайн-сборке лучше кэшировать архив или использовать системный пакет.
find_package(fmt CONFIG REQUIRED)
target_link_libraries(core PUBLIC fmt::fmt)
include(FetchContent)
FetchContent_Declare(googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.15.2
GIT_SHALLOW TRUE)
FetchContent_MakeAvailable(googletest)
Тег фиксируйте явно: плавающая ветка однажды сломает сборку. Если find_package находит не ту версию, укажите fmt_DIR или CMAKE_PREFIX_PATH вместо правки системных путей.
Сборка и установка
Конфигурация и сборка разделены. Это позволяет менять тип сборки и генератор, не удаляя исходники. Для одноконфигурационных генераторов, таких как Make и Ninja, тип задаётся при конфигурации.
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel
ctest --test-dir build --output-on-failure
sudo cmake --install build --prefix /usr/local
Для установки нужны правила install(TARGETS app RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) и include(GNUInstallDirs). Без них cmake --install ничего не сделает. Префикс можно не задавать через sudo, а сразу конфигурировать с -DCMAKE_INSTALL_PREFIX=$HOME/.local.
Отладочная и релизная сборки должны жить в разных каталогах. Если переключать CMAKE_BUILD_TYPE в одном build, CMake перегенерирует проект, но объектные файлы могут остаться от прошлой конфигурации. Два каталога — build/debug и build/release — избавляют от этой путаницы и позволяют держать оба варианта одновременно.
Тесты и CTest
CMake включает CTest — запуск тестов без стороннего раннера. Тест регистрируется командой add_test, а имя должно быть уникальным в пределах каталога. Для проекта на C++ удобно собрать отдельный исполняемый файл и передать его CTest.
include(CTest)
if(BUILD_TESTING)
add_executable(core_tests tests/core_tests.cpp)
target_link_libraries(core_tests PRIVATE core GTest::gtest_main)
add_test(NAME core_tests COMMAND core_tests)
endif()
CTest не пересобирает код: сначала запустите cmake --build, затем ctest. Параметр --output-on-failure показывает вывод только упавших тестов, а -j распараллеливает прогон. В CI эти две команды заменяют самописный скрипт и дают машинно-читаемый отчёт.
Кросс-компиляция
Для сборки под другую архитектуру не меняйте CMAKE_CXX_COMPILER вручную. Опишите компилятор, sysroot и флаги в отдельном toolchain-файле и передайте его через -DCMAKE_TOOLCHAIN_FILE=toolchains/aarch64.cmake. Так настройки остаются воспроизводимыми, а кэш не смешивает две разные платформы. Перед сменой toolchain всегда удаляйте каталог сборки: старые проверки компилятора приведут к трудночитаемым ошибкам.
Presets вместо длинных команд
CMakePresets.json хранит параметры конфигурации в репозитории и избавляет от повторения ключей. Файл понимают CMake, Visual Studio Code и CLion. Схема версии 6 требует CMake 3.25 или новее.
{
"version": 6,
"configurePresets": [
{
"name": "debug",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/debug",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
}
}
],
"buildPresets": [{"name": "debug", "configurePreset": "debug"}],
"testPresets": [{"name": "debug", "configurePreset": "debug",
"output": {"outputOnFailure": true}}]
}
Запуск: cmake --preset debug, затем cmake --build --preset debug и ctest --preset debug. Переменная ${sourceDir} раскрывается в каталог с файлом пресетов, поэтому конфигурация не зависит от места вызова.
Диагностика
CMake любит кэш: он хранит найденные компиляторы, пути и версии в CMakeCache.txt. Если вы сменили toolchain или перенесли каталог, старый кэш даст странные ошибки. Самый надёжный способ — удалить build и сконфигурировать заново.
cmake -LAH -N build | grep -E "CMAKE_BUILD_TYPE|CMAKE_CXX_COMPILER"
cmake -S . -B build --debug-find
cmake --build build --verbose
rm -rf build && cmake -S . -B build
--debug-find показывает, где CMake искал пакет и почему отверг найденный вариант. --verbose печатает полные команды компиляции и линковки. Для ошибок в собственном CMakeLists.txt помогает message(STATUS "...") и --trace-expand, но вывод последнего очень большой.
cmake -S . -B build --trace-expand 2>&1 | tail -n 40
grep -n "CMAKE_CXX_COMPILER:" build/CMakeCache.txt
Типичные грабли
- CMake 4.x не поддерживает
cmake_minimum_requiredниже 3.5 — обновите строку, а не понижайте CMake; - тип сборки игнорируется multi-config генераторами — передавайте
--config Releaseпри сборке; - абсолютные пути в
installломают перенос — используйтеGNUInstallDirs; - глобальные
include_directoriesмаскируют ошибки — переносите их на цели; - нехватка памяти при
--parallelлечится ограничением числа задач; - пустой каталог сборки после смены пресета — проверьте
binaryDirи не смешивайте конфигурации.
Итог
CMake стоит воспринимать как описание целей и их связей, а не как набор команд. Держите сборку вне исходников, задавайте зависимости у конкретных целей, фиксируйте версии и выносите параметры в presets. Тогда проект одинаково собирается локально, в CI и у коллеги на другой машине, а ошибки лечатся чтением кэша и трассировки, а не переустановкой компилятора.