Добавить в избранное

CMake для C++-проектов: сборка, зависимости и структура

Чтение ≈ 4 мин

CMake с нуля: минимальный CMakeLists.txt, исполняемые файлы и библиотеки, подключение зависимостей через find_package и FetchContent, таргеты и свойства, типичная структура C++-проекта.

Зачем 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 — это документация: по нему видно структуру, зависимости и опции проекта без чтения кода.

Поделиться
Понравился материал?

Комментарии

Добавить комментарий