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 и у коллеги на другой машине, а ошибки лечатся чтением кэша и трассировки, а не переустановкой компилятора.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте