CMake для C++ разработчика: от основ до Modern CMake

Практический курс по стандарту индустрии сборки C++ проектов. Вы освоите синтаксис, научитесь управлять зависимостями, применять современные практики (Target-based approach) и подготавливать проекты к релизу.

1. Введение в CMake: базовый синтаксис, структура CMakeLists.txt и сборка первого приложения

Введение в CMake: базовый синтаксис, структура CMakeLists.txt и сборка первого приложения

Добро пожаловать в курс CMake для C++ разработчика: от основ до Modern CMake. Мы начинаем наше путешествие с фундаментальных понятий. Если вы когда-либо пытались собрать сложный C++ проект вручную, прописывая десятки флагов компилятора, или мучились с переносом проекта из Visual Studio в Linux, то вы быстро поймете ценность CMake.

В этой статье мы разберем, что такое CMake, как он работает, изучим синтаксис языка и соберем наше первое приложение правильным способом.

Что такое CMake и зачем он нужен?

CMake (Cross-platform Make) — это кроссплатформенная система автоматизации сборки. Однако, важно сразу прояснить одно распространенное заблуждение: CMake не собирает ваш проект. Он не является компилятором (как g++ или clang) и не является системой сборки в привычном понимании (как Make или Ninja).

CMake — это генератор систем сборки.

Как это работает?

Вы описываете правила сборки вашего проекта в файле с именем CMakeLists.txt, используя специальный язык CMake. Затем вы запускаете CMake, и он, основываясь на ваших правилах и текущей операционной системе, генерирует файлы для конкретной системы сборки.

* В Linux он обычно генерирует Makefile. * В Windows он может сгенерировать решение (.sln) для Visual Studio. * В macOS он может создать проект Xcode.

!Схема, демонстрирующая роль CMake как генератора нативных проектов сборки

Это позволяет вам поддерживать всего один файл конфигурации (CMakeLists.txt) для всех платформ, вместо того чтобы вручную править Makefiles и файлы проектов IDE.

Базовый синтаксис CMake

Язык CMake — это скриптовый язык. Он достаточно прост, но имеет свои особенности. Все инструкции в CMake являются командами.

Формат команд

Команда выглядит следующим образом:

* Имена команд (например, project, add_executable) нечувствительны к регистру. Вы можете писать project(...), PROJECT(...) или Project(...). Однако общепринятым стандартом в Modern CMake является использование строчных букв (lowercase). * Аргументы разделяются пробелами или переносами строк. * Аргументы чувствительны к регистру. Имена файлов и переменных должны быть написаны точно так, как они есть.

Комментарии

Комментарии начинаются с символа # и продолжаются до конца строки.

Структура CMakeLists.txt

Файл CMakeLists.txt должен находиться в корне вашего проекта. Давайте создадим минимальный рабочий пример. Представьте, что у нас есть файл main.cpp:

Теперь создадим для него CMakeLists.txt. Минимальный файл состоит из трех обязательных команд.

1. Требование версии CMake

Первая строка любого CMakeLists.txt должна указывать минимально необходимую версию CMake.

Это гарантирует, что если у пользователя установлена слишком старая версия CMake, которая не поддерживает используемые вами функции, сборка остановится с понятной ошибкой. Версия 3.10 — это разумный минимум для большинства современных проектов, хотя в курсе мы будем ориентироваться на возможности версий 3.15+.

2. Определение проекта

Следующая команда задает имя проекта и, опционально, его версию и используемые языки.

* MyFirstProject — имя проекта. * VERSION 1.0 — устанавливает переменные версии проекта. * LANGUAGES CXX — явно указывает, что мы используем C++ (CXX в терминологии CMake означает C++).

3. Создание исполняемого файла

Наконец, мы должны сказать CMake, что мы хотим собрать.

* MyBinary — это имя цели (target). Именно так будет называться итоговый исполняемый файл (в Windows это будет MyBinary.exe, в Linux — MyBinary). * main.cpp — исходный файл, из которого собирается цель. Если файлов несколько, их можно перечислить через пробел: add_executable(MyBinary main.cpp utils.cpp).

Итоговый файл CMakeLists.txt:

Переменные в CMake

CMake поддерживает переменные, которые помогают сделать скрипты гибкими. Для установки значения переменной используется команда set.

Чтобы получить значение переменной, используется синтаксис {EXECUTABLE_NAME} ${SOURCE_FILES}) bash mkdir build cd build cmake .. bash cmake -S . -B build bash cmake --build build `

Эта команда универсальна. Неважно, используете ли вы Make, Ninja или MSBuild — CMake сам вызовет нужную утилиту с правильными флагами.

!Последовательность шагов: написание кода, конфигурация проекта и финальная сборка

Генераторы (Generators)

Как CMake узнает, для какой системы генерировать файлы? По умолчанию он выбирает наиболее подходящую для вашей ОС (например, Unix Makefiles для Linux или Visual Studio для Windows).

Однако вы можете явно указать Генератор с помощью флага -G.

Примеры:

* cmake -G "Unix Makefiles" .. * cmake -G "Ninja" .. (Ninja — очень быстрая система сборки, рекомендуемая для крупных проектов) * cmake -G "Visual Studio 16 2019" ..

Чтобы увидеть список доступных генераторов на вашей машине, просто введите cmake --help.

Заключение

Мы сделали первый шаг в освоении CMake. Теперь вы знаете, что:

  • CMake — это генератор, а не компилятор.
  • Файл конфигурации называется CMakeLists.txt.
  • Сборку нужно проводить в отдельной директории (Out-of-Source).
  • Процесс делится на конфигурацию (cmake -S ...) и сборку (cmake --build ...`).
  • В следующей статье мы углубимся в работу с целями (targets), узнаем, как подключать несколько исходных файлов и как правильно управлять библиотеками.

    2. Модульность проекта: создание статических и динамических библиотек, работа с подкаталогами

    Модульность проекта: создание статических и динамических библиотек, работа с подкаталогами

    В предыдущей статье мы научились создавать минимальный файл CMakeLists.txt и собирать простой исполняемый файл из одного исходника. Но реальные C++ проекты редко состоят из одного файла main.cpp. Обычно это десятки, сотни, а иногда и тысячи файлов, разбросанных по разным папкам.

    Чтобы не утонуть в хаосе кода, разработчики разбивают проект на логические модули — библиотеки. В этой статье мы перейдем от простых скриптов к архитектуре настоящего проекта. Мы узнаем, как создавать статические и динамические библиотеки, как связывать их с исполняемым файлом и как правильно организовать структуру папок с помощью add_subdirectory.

    Зачем нужна модульность?

    Представьте, что вы пишете игровой движок. У вас есть код для рендеринга графики, код для физики, код для работы со звуком и логика самой игры. Если свалить всё это в одну кучу, время компиляции при любом изменении будет огромным, а разобраться в коде станет невозможно.

    CMake позволяет выделить каждый компонент в отдельную библиотеку. Это дает следующие преимущества:

  • Ускорение сборки: Если вы меняете код в модуле физики, пересобирается только он и финальное приложение, а графика и звук не трогаются.
  • Переиспользование кода: Библиотеку можно подключить к разным проектам (например, движок один, а игр на нем — много).
  • Логическая изоляция: Четкое разделение зон ответственности.
  • !Визуализация того, как исполняемое приложение собирается из нескольких независимых библиотечных модулей.

    Работа с подкаталогами: add_subdirectory

    Типичная структура C++ проекта выглядит так:

    Обратите внимание: в каждой папке, где есть код, который нужно собрать, должен лежать свой CMakeLists.txt. Чтобы корневой CMake узнал о существовании вложенных папок, используется команда add_subdirectory.

    Корневой CMakeLists.txt

    Его задача — просто подключить папки в нужном порядке.

    Порядок важен: если src зависит от libs/math, лучше сначала добавить библиотеку, хотя CMake умеет разрешать зависимости целей (targets) независимо от порядка объявления, если они известны глобально.

    Создание библиотек: add_library

    Теперь заглянем в файл libs/math/CMakeLists.txt. Вместо add_executable мы будем использовать команду add_library.

    Синтаксис: add_library(<name> [STATIC | SHARED | MODULE] <sources>)

    Статические библиотеки (STATIC)

    Если указан тип STATIC, CMake создаст архив объектных файлов (.lib на Windows, .a на Linux/macOS). При сборке приложения код из этой библиотеки будет скопирован внутрь исполняемого файла.

    * Плюсы: Исполняемый файл самодостаточен, не нужно таскать с собой DLL/SO файлы. * Минусы: Размер исполняемого файла растет. Если библиотеку используют 10 программ, код библиотеки будет продублирован 10 раз на диске.

    Динамические библиотеки (SHARED)

    Если указан тип SHARED, CMake создаст динамическую библиотеку (.dll на Windows, .so на Linux, .dylib на macOS). При сборке в исполняемый файл помещается только ссылка на библиотеку.

    * Плюсы: Экономия места на диске и в оперативной памяти (ОС загружает код библиотеки в память один раз для всех процессов). * Минусы: Вместе с .exe файлом нужно распространять и .dll файлы. Если ОС не найдет библиотеку, программа не запустится.

    Если тип не указан (просто add_library(Name source.cpp)), CMake по умолчанию создаст статический библиотеку (обычно), но этим поведением можно управлять через переменную BUILD_SHARED_LIBS.

    Связывание целей: target_link_libraries

    Мы создали библиотеку GeometryLib. Теперь перейдем в src/CMakeLists.txt и научим наше приложение использовать её.

    Команда target_link_libraries делает две вещи:

  • Передает компоновщику (linker) путь к файлу библиотеки, чтобы он мог найти скомпилированные функции.
  • (В Modern CMake) Автоматически пробрасывает пути к заголовочным файлам, если они правильно настроены (об этом ниже).
  • Ключевое слово PRIVATE означает, что MyApp использует GeometryLib, но если бы MyApp само было библиотекой, то его пользователям GeometryLib была бы не нужна. Также существуют PUBLIC и INTERFACE.

    !Схематическое изображение того, как компоновщик объединяет объектные файлы и библиотеки в единый исполняемый файл.

    Проблема заголовочных файлов (Include Directories)

    В C++, чтобы использовать функции из библиотеки, нужно подключить её заголовочный файл:

    Файл geometry.h лежит в папке libs/math, а main.cpp — в src. Компилятор ищет файлы только в текущей папке и системных путях. Нам нужно добавить путь к заголовкам.

    Старый подход (не рекомендуется)

    Раньше использовали команду include_directories(../libs/math). Это глобальная команда, которая добавляет путь поиска для всех целей в текущей папке и подпапках. Это загрязняет проект.

    Modern CMake подход: target_include_directories

    В современном CMake мы настраиваем пути поиска для конкретной цели (target). Более того, библиотека сама должна сказать: "Мои заголовочные файлы лежат здесь".

    Вернемся в libs/math/CMakeLists.txt и улучшим его:

    cpp #pragma once int add(int a, int b); cpp #include "geometry.h" int add(int a, int b) { return a + b; } cmake add_library(GeometryLib STATIC geometry.cpp) target_include_directories(GeometryLib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) cpp #include <iostream> #include "geometry.h" // Теперь это работает!

    int main() { std::cout << "2 + 2 = " << add(2, 2) << std::endl; return 0; } cmake add_executable(MyApp main.cpp) target_link_libraries(MyApp PRIVATE GeometryLib) cmake cmake_minimum_required(VERSION 3.10) project(ModularApp LANGUAGES CXX)

    add_subdirectory(libs/math) add_subdirectory(src) ``

    Теперь, запустив сборку из корня (cmake -S . -B build && cmake --build build), вы получите рабочий проект с правильной модульной структурой.

    Заключение

    Мы сделали огромный шаг вперед. Теперь вы умеете: * Разбивать проект на подкаталоги с помощью add_subdirectory. * Создавать библиотеки командой add_library. * Понимать разницу между STATIC и SHARED. * Связывать компоненты через target_link_libraries. * Правильно пробрасывать пути к заголовкам, используя PUBLIC` область видимости.

    В следующей статье мы разберем, как управлять зависимостями, находить установленные в системе библиотеки (например, Boost или OpenCV) и подключать их к вашему проекту.

    3. Управление зависимостями: подключение сторонних библиотек и использование find_package

    Управление зависимостями: подключение сторонних библиотек и использование find_package

    В предыдущих статьях мы научились создавать структуру проекта, разбивать его на модули и связывать собственные библиотеки с исполняемым файлом. Однако сила C++ заключается не только в языке, но и в огромной экосистеме готовых библиотек. Boost, OpenCV, Qt, fmt, nlohmann_json — эти инструменты экономят тысячи часов разработки.

    Но как объяснить CMake, что наш проект зависит от библиотеки, которая уже установлена где-то в системе? Мы не хотим копировать исходный код гигантской библиотеки (например, Qt) в нашу папку проекта. Мы хотим найти её и подключить.

    Для этого в CMake существует мощнейший механизм — команда find_package.

    Проблема внешних зависимостей

    Когда вы пишете add_subdirectory(libs/mylib), CMake полностью контролирует процесс сборки этой библиотеки, потому что она является частью вашего дерева исходных кодов. Но внешние библиотеки (External Dependencies) живут своей жизнью:

  • Они уже скомпилированы (часто).
  • Они находятся в системных путях (например, /usr/lib на Linux или C:\Program Files на Windows).
  • У них свои заголовочные файлы и свои правила линковки.
  • Ручное прописывание путей (например, include_directories("C:/Boost/include")) — это плохая практика. Это делает ваш CMakeLists.txt непереносимым: на компьютере вашего коллеги Boost может быть установлен в другую папку.

    Нам нужен механизм, который сам найдет библиотеку, независимо от ОС и путей установки.

    !CMake сканирует системные пути, чтобы найти конфигурационные файлы установленных библиотек.

    Команда find_package

    Команда find_package — это стандартный способ поиска внешних зависимостей в CMake. В самом простом виде она выглядит так:

    Разберем аргументы:

    * Boost — имя пакета (библиотеки), который мы ищем. Оно чувствительно к регистру. * 1.70 — (опционально) минимально требуемая версия. Если в системе найден Boost 1.65, CMake выдаст ошибку. * REQUIRED — (опционально, но рекомендуется) ключевое слово, означающее, что библиотека обязательна. Если CMake не найдет её, конфигурация проекта остановится с фатальной ошибкой. Если это слово опустить, CMake просто предупредит, что библиотека не найдена, и продолжит работу (это полезно для опциональных фич).

    Как это работает под капотом?

    Когда вы вызываете find_package(Abc), CMake пытается найти файл, описывающий эту библиотеку. Поиск идет в двух режимах:

  • Module Mode: CMake ищет файл FindAbc.cmake. Обычно такие файлы поставляются вместе с самим CMake (в папке установки CMake есть сотни модулей для популярных библиотек: Boost, Curl, OpenGL и т.д.).
  • Config Mode: Если модуль не найден, CMake ищет файл AbcConfig.cmake или abc-config.cmake. Эти файлы обычно поставляются авторами самой библиотеки и устанавливаются вместе с ней в систему.
  • Вам, как пользователю, редко нужно думать о режиме поиска. Главное — результат: если пакет найден, CMake предоставляет вам переменные или цели (targets) для использования.

    Modern CMake: Использование импортированных целей

    В старых версиях CMake (до 3.0) после вызова find_package вам приходилось оперировать переменными. Это выглядело примерно так (сейчас так делать не надо):

    В чем проблема? Переменные {Boost_LIBRARIES} — это просто списки путей и файлов. Они не несут информации о том, какие флаги компилятора нужны, какие зависимости тянет за собой сам Boost.

    Modern CMake использует концепцию Imported Targets (Импортированные цели). Когда find_package находит библиотеку, он создает псевдо-цель, которая ведет себя как обычная библиотека, созданная через add_library.

    Правильный современный подход:

    Обратите внимание на Boost::filesystem. Это импортированная цель. Когда вы линкуете её:

  • CMake автоматически добавляет путь к заголовочным файлам Boost (вам не нужен include_directories).
  • CMake автоматически добавляет нужные .lib или .so файлы компоновщику.
  • CMake автоматически добавляет необходимые флаги компиляции и препроцессора.
  • Компоненты (COMPONENTS)

    Многие библиотеки (как Boost, Qt, SFML) модульные. Вам редко нужен весь Boost. Ключевое слово COMPONENTS позволяет указать, какие именно части библиотеки вам нужны.

    Если хотя бы один из компонентов не будет найден, CMake выдаст ошибку (из-за флага REQUIRED).

    Пример: Подключение fmt

    Библиотека fmt — это современный стандарт форматирования строк в C++. Допустим, она установлена в вашей системе. Подключим её.

    CMakeLists.txt:

    main.cpp:

    Всё! Нам не нужно знать, где именно установлен fmt — в /usr/local/include или в C:/Program Files/fmt/include. CMake берет это на себя.

    Что делать, если CMake не находит библиотеку?

    Это самая частая проблема новичков. Вы установили библиотеку, написали find_package, но получаете ошибку:

    Это происходит, если библиотека установлена в нестандартное место (например, вы скачали её в папку Downloads или установили в D:/Libs). CMake ищет только в стандартных системных путях.

    Чтобы помочь CMake, используйте переменную CMAKE_PREFIX_PATH. Это список путей, где CMake должен искать пакеты до того, как пойдет в системные папки.

    Вы можете передать этот путь при конфигурации в терминале:

    Или (менее рекомендуется, но возможно) установить её прямо в CMakeLists.txt перед вызовом find_package:

    !Алгоритм поиска пакетов: сначала проверяется пользовательский путь CMAKE_PREFIX_PATH, затем стандартные системные директории.

    Как узнать имя пакета и цели?

    Откуда я узнал, что нужно писать fmt::fmt или Boost::filesystem? К сожалению, единого стандарта именования нет.

  • Документация библиотеки: Хорошие библиотеки пишут в README раздел "CMake Integration".
  • Поиск в интернете: Запрос "cmake link boost" или "cmake find_package opencv example".
  • Команда cmake --help-module: Если пакет ищется через встроенный модуль CMake, можно почитать справку.
  • Вывод переменных: После успешного find_package CMake часто выводит сообщение, но вы также можете проверить переменные:
  • Стандартные переменные

    Даже если вы используете Modern CMake, полезно знать, какие переменные устанавливает find_package. Обычно (но не всегда!) это:

    * <PackageName>_FOUNDTRUE, если пакет найден. * <PackageName>_INCLUDE_DIRS — пути к заголовкам. * <PackageName>_LIBRARIES — пути к библиотекам. * <PackageName>_VERSION — найденная версия.

    Пример проверки:

    Резюме

    Использование сторонних библиотек — неотъемлемая часть разработки. CMake делает этот процесс унифицированным.

  • Используйте find_package(Name REQUIRED) для поиска зависимостей.
  • Всегда предпочитайте Imported Targets (например, Boost::filesystem) старым переменным (Boost_LIBRARIES).
  • Используйте COMPONENTS, чтобы загружать только нужные части больших библиотек.
  • Если библиотека не находится, укажите путь к ней через -DCMAKE_PREFIX_PATH="..." при конфигурации.
  • Теперь ваш проект может использовать мощь всей индустрии C++. Но что делать, если библиотеки нет в системе, и вы хотите, чтобы CMake скачал и собрал её сам? Об этом мы поговорим в следующих статьях, посвященных FetchContent и пакетным менеджерам.

    4. Философия Modern CMake: целевой подход, транзитивные свойства и генераторные выражения

    Философия Modern CMake: целевой подход, транзитивные свойства и генераторные выражения

    В предыдущих статьях мы научились собирать проекты, создавать библиотеки и подключать внешние зависимости. Вы уже использовали команды вроде target_link_libraries и target_include_directories. Однако, чтобы стать настоящим профессионалом в CMake, недостаточно просто знать названия команд. Нужно понимать философию, стоящую за ними.

    До версии 3.0 (выпущенной в 2014 году) CMake работал совсем иначе. Этот старый стиль часто называют «Legacy CMake». Современный подход, который мы изучаем, называется Modern CMake. Главное отличие между ними — это смещение фокуса с глобальных переменных на Цели (Targets).

    В этой статье мы разберем три кита Modern CMake: целевой подход, транзитивность свойств и генераторные выражения.

    Целевой подход (Target-Centric Approach)

    В старом CMake (Legacy) разработчики оперировали глобальным состоянием. Скрипт выглядел как последовательность инструкций: «добавь эту папку в пути поиска заголовков», «добавь этот флаг компилятора», «собери вот эти файлы».

    Это было похоже на использование глобальных переменных в C++. Если вы меняли флаг компиляции в одном месте, он мог неожиданно примениться ко всему проекту, ломая сборку в совершенно другой части.

    Modern CMake работает иначе. Он объектно-ориентирован. Основной единицей здесь является Цель (Target).

    Что такое Цель?

    Цель — это объект внутри CMake, который представляет собой создаваемый артефакт (исполняемый файл или библиотеку). Когда вы пишете:

    Вы создаете объект цели. У этого объекта есть свойства (Properties):

    * Исходные файлы * Флаги компиляции * Пути к заголовочным файлам (Include directories) * Зависимости (Link libraries) * Определения препроцессора (Compile definitions)

    Вместо того чтобы разбрасывать настройки по всему файлу, мы инкапсулируем их внутри конкретной цели.

    !Визуализация цели как контейнера, инкапсулирующего свои свойства и настройки сборки.

    Почему это важно?

    Представьте, что у вас в проекте две библиотеки: LibA (требует флаг -std=c++17) и LibB (старая, требует -std=c++98).

    * В Legacy CMake вы бы установили глобальный флаг, и одна из библиотек перестала бы собираться. * В Modern CMake вы устанавливаете свойство CXX_STANDARD для каждой цели отдельно. Они не конфликтуют.

    Именно поэтому мы используем команды с префиксом target_:

    * target_include_directories вместо include_directories * target_compile_options вместо add_compile_options * target_compile_definitions вместо add_definitions

    Транзитивные свойства (Transitive Usage Requirements)

    Вторая важнейшая концепция — это транзитивность. Это способность CMake автоматически передавать требования от одной цели к другой по цепочке зависимостей.

    Когда мы пишем target_link_libraries(App PRIVATE LibA), мы говорим: «Приложение App зависит от LibA». Но что это значит на практике? Это значит, что для успешной компиляции и линковки App может потребоваться:

  • Сам файл библиотеки LibA (для линковщика).
  • Пути к заголовочным файлам LibA (для компилятора).
  • Определенные флаги компиляции, необходимые для LibA.
  • Управление этой передачей осуществляется через ключевые слова PRIVATE, PUBLIC и INTERFACE. Давайте разберем их механику окончательно.

    Механика распространения

    У каждой цели есть два набора свойств:

  • Build Requirements: То, что нужно для сборки самой цели.
  • Usage Requirements: То, что нужно потребителям этой цели.
  • Ключевые слова управляют тем, куда попадают настройки:

    | Ключевое слово | Build Requirements (Для меня) | Usage Requirements (Для других) | | :--- | :---: | :---: | | PRIVATE | Да | Нет | | INTERFACE | Нет | Да | | PUBLIC | Да | Да |

    Пример из жизни

    Допустим, вы пишете библиотеку Engine, которая использует внутри себя библиотеку Boost, а наружу предоставляет API, использующее типы из библиотеки GLM.

    Теперь, когда кто-то создаст приложение:

    Произойдет магия транзитивности:

  • Game автоматически получит пути к хедерам Engine.
  • Game автоматически получит пути к хедерам GLM (потому что связь PUBLIC).
  • Game НЕ получит пути к Boost (потому что связь PRIVATE).
  • Это позволяет строить чистые и модульные системы, где детали реализации скрыты, а необходимые интерфейсы пробрасываются автоматически.

    !Схема транзитивной передачи свойств зависимостей через ключевые слова PUBLIC и PRIVATE.

    Генераторные выражения (Generator Expressions)

    Третий столп Modern CMake — это генераторные выражения. Это, пожалуй, самая сложная для понимания, но невероятно мощная тема.

    Проблема этапов

    Вспомним, что CMake работает в два этапа:

  • Конфигурация: Чтение CMakeLists.txt, создание внутренней модели проекта.
  • Генерация: Создание реальных файлов сборки (Makefile, .sln).
  • Обычные переменные if(VAR) вычисляются на этапе конфигурации. Но иногда нам нужна логика, которая зависит от того, что известно только на этапе генерации или даже сборки. Например:

    «Добавь этот флаг, только если мы собираем Debug версию» (в Visual Studio тип сборки выбирается уже после* генерации). * «Используй этот путь к хедерам, если мы собираем библиотеку, но вот этот путь, если мы её устанавливаем в систему».

    Синтаксис

    Генераторные выражения имеют вид <CONDITION:VALUE> — если условие истинно, подставить VALUE, иначе пустую строку.

    Примеры использования

    #### 1. Флаги для конкретной конфигурации

    Мы хотим включить отладочную печать, но только в Debug сборке.

    Если пользователь собирает Release, это выражение превратится в пустоту. Если Debug — в ENABLE_DEBUG_LOGGING.

    #### 2. Специфика компилятора

    Мы хотим включить строгие предупреждения, но флаги у GCC и MSVC разные.

    #### 3. Build Interface vs Install Interface

    Это классическая проблема создателей библиотек.

    * Когда мы собираем библиотеку сами, заголовочные файлы лежат в папке src/include. * Когда библиотеку установили (make install), заголовочные файлы переехали в /usr/local/include.

    Как сказать потребителю библиотеки, где искать хедеры, чтобы это работало в обоих случаях?

    Это выражение позволяет одной строкой описать поведение для двух разных жизненных циклов проекта.

    INTERFACE библиотеки

    В завершение темы целевого подхода стоит упомянуть особый тип библиотек: INTERFACE.

    Такая библиотека не создает никаких файлов .a или .lib. Она не компилируется. Это просто виртуальная цель — контейнер для свойств.

    Это идеально подходит для:

  • Header-only библиотек (библиотек, состоящих только из заголовков). Вы прикрепляете к ней target_include_directories, и любой, кто слинкуется с ней, получит эти пути.
  • Группировки настроек. Вы можете создать цель ProjectWarnings, прописать в ней все нужные флаги компилятора, а потом просто линковать её ко всем своим исполняемым файлам: target_link_libraries(App PRIVATE ProjectWarnings). Это очень элегантный способ управления общими настройками.
  • Заключение

    Философия Modern CMake заключается в отказе от глобального хаоса в пользу структурированных, изолированных модулей.

  • Думайте целями: Всё есть цель. Настраивайте цель, а не проект.
  • Инкапсулируйте: Используйте PRIVATE, чтобы скрыть детали реализации.
  • Пробрасывайте: Используйте PUBLIC и INTERFACE, чтобы автоматически передавать зависимости потребителям.
  • Уточняйте: Используйте генераторные выражения для тонкой настройки под разные условия сборки.
  • Освоив эти принципы, вы перестанете бороться с системой сборки и начнете использовать её как мощный инструмент архитектуры вашего C++ проекта.

    5. Завершение цикла разработки: тестирование с CTest, инсталляция и упаковка через CPack

    Завершение цикла разработки: тестирование с CTest, инсталляция и упаковка через CPack

    Поздравляю! Если вы дошли до этой статьи, значит, вы уже умеете писать CMakeLists.txt, создавать библиотеки, управлять зависимостями и понимаете философию Modern CMake. Ваш проект собирается. Но готов ли он к выпуску?

    В профессиональной разработке «код компилируется» — это только половина дела. Чтобы ваш продукт стал полноценным, нужно ответить на три вопроса:

  • Работает ли он правильно? (Тестирование)
  • Как пользователю установить его в систему? (Инсталляция)
  • Как передать его пользователю? (Упаковка)
  • CMake предоставляет встроенные инструменты для решения всех этих задач: CTest для тестирования и CPack для создания установщиков. В этой статье мы замкнем цикл разработки.

    !Визуализация полного цикла сборки и поставки программного обеспечения с использованием инструментов CMake.

    Тестирование с CTest

    CTest — это инструмент для запуска тестов, который поставляется вместе с CMake. Важно понимать: CTest сам по себе не пишет тесты. Он не заменяет фреймворки вроде GoogleTest или Catch2. CTest — это менеджер запуска.

    Его задача — запустить указанные вами исполняемые файлы, проверить код возврата (0 — успех, не 0 — провал) и сформировать отчет.

    Базовая настройка

    Чтобы включить поддержку тестирования, в корневом CMakeLists.txt нужно добавить всего одну команду:

    Эта команда создает необходимые файлы конфигурации для CTest в папке сборки. Обычно её вызывают в самом конце корневого файла.

    Добавление простого теста

    Самый примитивный тест — это просто запуск вашей программы. Если она не упала (returned 0), тест пройден.

    * NAME — имя теста, которое будет отображаться в отчете. * COMMAND — что именно запустить. Здесь MyApp — это имя цели (target), созданной через add_executable. CMake автоматически подставит полный путь к скомпилированному файлу.

    Запуск тестов

    После того как вы сконфигурировали проект и собрали его, перейдите в папку сборки (build) и выполните команду:

    Вы увидите вывод вроде этого:

    Для более подробного вывода (чтобы видеть, что именно программа писала в консоль) используйте флаг --output-on-failure или -V (verbose).

    Интеграция с GoogleTest (GTest)

    В реальном мире мы пишем unit-тесты с использованием фреймворков. Самый популярный в C++ — GoogleTest. CMake имеет великолепную интеграцию с ним.

    Предположим, у вас есть файл с тестами tests.cpp:

    Вот как выглядит CMakeLists.txt для подключения GTest:

    Команда gtest_discover_tests — это магия Modern CMake. Она запускает ваш исполняемый файл UnitTests, узнает список всех написанных внутри тестов и регистрирует каждый из них в CTest отдельно. Это значит, что в отчете вы увидите не просто «UnitTests passed», а «MathTest.Addition passed».

    Инсталляция (Install)

    Когда вы собираете проект, бинарные файлы лежат глубоко в папке build. Пользователю (или системному администратору) неудобно искать их там. Процесс инсталляции копирует нужные файлы (исполняемые файлы, библиотеки, хедеры) в чистую, структурированную директорию.

    В CMake за это отвечает команда install.

    Куда устанавливать?

    Место установки контролируется переменной CMAKE_INSTALL_PREFIX. * В Linux по умолчанию это /usr/local. * В Windows — C:/Program Files/${PROJECT_NAME}.

    Вы можете изменить это при конфигурации:

    Команда install

    Внутри CMakeLists.txt мы должны явно сказать, что мы хотим отдать пользователю. Мы не хотим отдавать временные объектные файлы или исходный код.

    Разберем аргументы: * TARGETS — перечисление целей, которые нужно установить. * RUNTIME DESTINATION — куда класть исполняемые файлы (обычно папка bin). * LIBRARY DESTINATION — куда класть динамические библиотеки (обычно папка lib). * ARCHIVE DESTINATION — куда класть статические библиотеки (тоже lib).

    Также часто нужно установить заголовочные файлы (для библиотек):

    Запуск инсталляции

    Инсталляция — это отдельный шаг после сборки:

    После выполнения этой команды в папке, указанной в CMAKE_INSTALL_PREFIX, появится красивая структура:

    Упаковка с CPack

    Теперь, когда мы умеем устанавливать проект по папкам, мы хотим передать его пользователю одним файлом: установщиком .msi для Windows, пакетом .deb для Ubuntu или просто архивом .zip.

    Здесь на сцену выходит CPack. Это инструмент упаковки, встроенный в CMake. Он использует информацию из команд install, которые мы написали выше.

    Настройка CPack

    Настройка CPack обычно делается в самом конце корневого CMakeLists.txt. Вам нужно просто установить несколько переменных и подключить модуль.

    Генераторы CPack

    CPack поддерживает множество форматов (Генераторов):

    * ZIP, TGZ: Простые архивы. * DEB, RPM: Пакеты для Linux дистрибутивов. * NSIS: Классический установщик Windows (требует установленного NSIS). * WIX: Создание .msi пакетов. * DragNDrop: Образы .dmg для macOS.

    Создание пакета

    После сборки проекта, запустите CPack из папки build:

    Или укажите конкретный генератор:

    В результате в папке сборки появится файл MySuperApp-1.0.0-Linux.zip (или другое имя в зависимости от настроек), который содержит всё, что вы описали в командах install.

    > "CPack — это кнопка 'Сделать хорошо'. Он берет вашу сложную структуру сборки и превращает её в один файл, который не стыдно отдать пользователю."

    Итоговый Workflow

    Давайте соберем всё вместе. Полный цикл работы над релизом выглядит так:

  • Конфигурация:
  • cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
  • Сборка:
  • cmake --build build
  • Тестирование:
  • cd build && ctest (Если тесты упали — релиз отменяется!)
  • Упаковка:
  • cpack

    Теперь у вас есть готовый артефакт.

    Заключение курса

    Мы прошли долгий путь от написания первого CMakeLists.txt до создания профессионального инсталлятора.

    Вы узнали: * Как описывать цели и зависимости. * Как структурировать проект с подкаталогами. * Как использовать Modern CMake подход (Targets & Properties). * Как подключать внешние библиотеки через find_package. * Как тестировать и упаковывать результат.

    CMake — это стандарт де-факто в мире C++. Это сложный, но мощный инструмент. Знания, полученные в этом курсе, позволят вам уверенно чувствовать себя в любом современном C++ проекте, будь то маленькая утилита или огромный энтерпрайз-продукт.

    Удачи в сборке!