CMake на практике: рабочие приёмы для C++ проектов
Как организовать сборку C++ через CMake, чтобы проект собирался у коллеги без правки путей: target-based подход, поиск зависимостей и типичные грабли с переменными и глобальными флагами.

Проект собирается у вас, но у коллеги падает на find_package, а в CI линковка ругается на неопределённые символы. Причина почти всегда одна: смешаны два стиля CMake — старый, на глобальных переменных и include_directories, и новый, target-based. Разберём, что советуют делать на практике для CMake 3.15 и новее, и чем каждый приём оплачивается.
Минимальный каркас, который не стыдно показать
Базовый CMakeLists.txt для библиотеки с исполняемым файлом выглядит так. Он использует target-based команды — всё, что нужно цели, привязано к самой цели, а не разбросано по глобальному состоянию.
cmake_minimum_required(VERSION 3.15)
project(myapp LANGUAGES CXX)
# Стандарт задаём как свойство целей, а не глобально
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Библиотека
add_library(core
src/parser.cpp
src/engine.cpp
)
# Заголовки библиотеки: PUBLIC — видны и потребителям цели
target_include_directories(core
PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
)
target_compile_features(core PUBLIC cxx_std_17)
# Исполняемый файл, который линкуется с библиотекой
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE core)
Ключевая идея — target_link_libraries(myapp PRIVATE core) подтягивает не только сам объектный файл, но и include-директории, флаги и определения, помеченные у core как PUBLIC или INTERFACE. Вам не нужно вручную дублировать пути. Это и есть то, ради чего стоит держаться target-based стиля.
PUBLIC, PRIVATE, INTERFACE — что выбрать
Три ключевых слова определяют, кому достаётся свойство: самой цели, её потребителям или обоим.
- PRIVATE — нужно только при сборке этой цели. Приватная зависимость, которую не видно в публичных заголовках.
- INTERFACE — нужно только потребителям, самой цели не нужно. Типично для header-only библиотек.
- PUBLIC — нужно и цели, и потребителям. Например, тип из зависимости фигурирует в публичном заголовке.
Практическое правило: если тип из библиотеки X появляется в вашем .hpp, который включают другие, — это PUBLIC. Если X используется только внутри .cpp — PRIVATE. Ошибка в этом месте не ломает сборку сразу, но заставляет потребителей вашей цели вручную линковать транзитивные зависимости.
Современный CMake — это описание графа целей и их требований, а не последовательность команд компилятору. Как только вы начинаете думать флагами, а не целями, проект перестаёт переноситься.
Зависимости: три способа и их цена
Самый частый вопрос на практике — как подключить чужую библиотеку. Есть три рабочих подхода, и выбор зависит от того, кто и где собирает проект.
| Подход | Что выигрываете | Чем платите |
|---|---|---|
find_package | Быстрая сборка, зависимость ставится системно | Библиотека должна быть установлена заранее; версии могут расходиться между машинами |
FetchContent | Зависимость собирается вместе с проектом, версия зафиксирована | Дольше первая сборка; чужой CMakeLists может тянуть лишнее |
| Менеджер (vcpkg, Conan) | Воспроизводимость, кэш бинарников, изоляция версий | Отдельный инструмент в цепочке, порог входа для новичков |
find_package — когда зависимость уже в системе
Если библиотека поставляет CMake-конфиг (большинство современных так делают), подключение сводится к двум строкам:
find_package(fmt 9 REQUIRED)
target_link_libraries(myapp PRIVATE fmt::fmt)
Обратите внимание на цель fmt::fmt с двойным двоеточием — это импортированная цель. Она несёт свои include-пути и флаги, вам не нужно писать target_include_directories для fmt вручную. Если пишете так — вы делаете лишнюю и вредную работу.
FetchContent — зависимость едет вместе с проектом
Подходит, когда нельзя рассчитывать на системную установку. CMake скачает исходники при конфигурации и добавит их как подпроект.
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 9.1.0 # фиксируем тег, а не ветку
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(myapp PRIVATE fmt::fmt)
Важно фиксировать конкретный тег или коммит, а не master: иначе сборка перестанет быть воспроизводимой, и однажды CI упадёт из-за изменений в апстриме, которых вы не делали.
Что чаще всего ломается
Ниже — ошибки, которые встречаются в чужих проектах чаще прочего, с симптомом и причиной.
- Глобальные
include_directories()иlink_libraries(). Пути утекают во все цели файла и наследуются подпроектами. Симптом — цель видит заголовки, которые не должна, а после рефакторинга сборка ломается непредсказуемо. Лечение — заменить наtarget_*варианты. - Флаги через
set(CMAKE_CXX_FLAGS ...). Затирают флаги, которые ставит toolchain или пользователь. Симптом — исчезают оптимизации в Release или ломается сборка под другой компилятор. Лечение —target_compile_optionsс генераторными выражениями по конфигурации. - Список исходников через
file(GLOB ...). CMake не пересканирует директорию при добавлении нового файла — новый.cppпросто не попадёт в сборку. Симптом — «у меня файл есть, а он не компилируется». Перечисляйте файлы явно. - Захардкоженный тип сборки. Если не задан
CMAKE_BUILD_TYPE, для однокофигурационных генераторов (Makefiles, Ninja) сборка идёт без оптимизаций и без отладочной информации. Симптом — Release-бинарник медленный. Задавайте тип явно при конфигурации:-DCMAKE_BUILD_TYPE=Release. - Смешение стилей в одном файле. Половина зависимостей через target-based, половина через глобальные переменные. Симптом — часть транзитивных зависимостей теряется, линковщик ругается на неопределённые символы у коллеги, но не у вас.
Флаги предупреждений — но не глобально
Частый соблазн — включить -Wall -Wextra глобально. Это накроет и чужие заголовки из зависимостей, которые вы не контролируете. Прикрепляйте предупреждения к своим целям и разделяйте по компилятору генераторным выражением:
target_compile_options(core PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4>
$<$<NOT:$<CXX_COMPILER_ID:MSVC>>:-Wall -Wextra -Wpedantic>
)
Здесь $<CXX_COMPILER_ID:MSVC> раскрывается в 1 под MSVC и в 0 под остальными — это генераторное выражение, оно вычисляется на этапе генерации, а не конфигурации. Флаги PRIVATE, потому что ваши предупреждения не должны навязываться потребителям core.
Пресеты вместо простыни ключей
Начиная с CMake 3.19 есть CMakePresets.json — способ зафиксировать наборы параметров конфигурации, чтобы команда не передавала -D вручную. Это отдельная большая тема; здесь достаточно знать, что пресеты решают проблему «у всех разные ключи конфигурации» и стоит их посмотреть в официальной документации, если проект собирают несколько человек.
Что настроить дальше
Каркас у вас есть. Следующие шаги, которые окупаются на реальном проекте:
- Добавьте
CMakePresets.json, чтобы конфигурация была одинаковой у всех и в CI. - Подключите тесты через
enable_testing()иadd_test— это база для запуска черезctest. - Если проект — библиотека для внешних потребителей, разберитесь с
install()и генерацией package-config файла: без этого вашfind_packageу чужих людей не заработает. Установка и экспорт целей — отдельная тема, здесь мы её не разворачивали. - Держите один стиль во всём дереве: если увидели в чужом модуле
include_directories— это кандидат на рефакторинг.
Поиск источников: Реальные статьи и книги со ссылками, библиография по ГОСТ
Спросить за 15 ₽Источники: CMake Documentation — cmake-presets(7), CMake Documentation — Latest
Частые вопросы
У меня CMake старше 3.15, приёмы сработают?
find_package не находит библиотеку, что делать?
Почему новый .cpp не компилируется, хотя я его добавил?
Собирается у меня, но падает на линковке в CI — почему?
Стоит ли включать -Wall -Wextra на весь проект?
FetchContent или менеджер пакетов вроде vcpkg — что выбрать?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.