Начальный курс по CMake

Изучите основы кроссплатформенной сборки C/C++ проектов с помощью CMake, ставшего стандартом де-факто в индустрии [eax.me](https://eax.me/2017/2017-05-10-cmake.html). Курс охватывает базовый синтаксис [pvsm.ru](https://www.pvsm.ru/kompilyatsiya/300549), структуру проекта [habr.com](https://habr.com/ru/articles/904992/) и современные практики разработки.

1. Введение в CMake и базовая структура проекта

Введение в CMake и базовая структура проекта

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

> CMake — это кроссплатформенная утилита для автоматической генерации скриптов сборки программы из исходного кода. Сама по себе она не компилирует код, а выступает в роли дирижера для других инструментов.

Мета-система сборки

Важно понимать фундаментальное отличие CMake от классических компиляторов. CMake является генератором систем сборки (meta-build system). Он читает конфигурационные файлы и создает инструкции для нативных утилит сборки, таких как Make, Ninja, Xcode или Visual Studio.

Процесс превращения исходного кода в готовую программу с использованием CMake делится на четкие этапы:

  • Написание конфигурационного файла с правилами проекта.
  • Этап конфигурации (генерация файлов для конкретной системы сборки).
  • Этап компиляции (непосредственный вызов компилятора через сгенерированные файлы).
  • Этап компоновки (объединение скомпилированных объектов в единый исполняемый файл).
  • Для наглядности сравним традиционный подход и использование генератора:

    | Характеристика | Прямой вызов компилятора (g++ / clang) | Использование CMake | | --- | --- | --- | | Масштабируемость | Низкая (сложно управлять десятками файлов) | Высокая (поддерживает проекты с тысячами файлов) | | Кроссплатформенность | Требует ручного изменения флагов под каждую ОС | Автоматическая адаптация под Windows, Linux, macOS | | Интеграция с IDE | Отсутствует | Нативная поддержка в CLion, Visual Studio, VS Code | | Управление зависимостями | Ручное скачивание и указание путей | Автоматический поиск и подключение библиотек |

    Анатомия конфигурационного файла

    Сердцем любого проекта, использующего эту технологию, является текстовый файл с названием CMakeLists.txt. Он всегда располагается в корневой директории исходного кода и содержит набор команд, описывающих структуру приложения.

    Рассмотрим минимально жизнеспособный пример такого файла:

    Каждая строка здесь выполняет строго определенную функцию. Команда cmake_minimum_required защищает проект от использования устаревших версий утилиты, которые могут не поддерживать современные функции. Команда project задает имя решения, которое будет отображаться в средах разработки. Переменная CMAKE_CXX_STANDARD указывает компилятору использовать стандарт языка C++20. Наконец, add_executable — это ключевая директива, которая приказывает создать исполняемый файл с именем my_app из исходного файла main.cpp.

    Если файл main.cpp содержит 150 строк кода, директива add_executable автоматически найдет нужный компилятор в системе, передаст ему этот файл и проконтролирует создание бинарного файла размером, например, 45 килобайт.

    Правильная организация директорий

    Для успешного масштабирования проекта критически важно с самого начала заложить правильную архитектуру папок. В мире C++ существует концепция out-of-source build (сборка вне источника). Это означает, что файлы, генерируемые в процессе компиляции, никогда не должны смешиваться с исходным кодом.

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

    Разделение ответственности между папками делает проект читаемым и безопасным для системы контроля версий:

    * src — содержит файлы реализации с расширением .cpp. Это логика работы программы. * include — хранит заголовочные файлы с расширением .h или .hpp. Они описывают интерфейсы и структуры данных. * build — пустая директория, предназначенная исключительно для артефактов сборки. Ее принято добавлять в исключения (например, в .gitignore).

    Использование папки build позволяет легко очистить проект. Если сборка сломалась или нужно начать все с чистого листа, достаточно просто удалить эту папку и создать ее заново. Исходный код при этом останется нетронутым.

    Запуск процесса конфигурации и компиляции

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

    Перейдя в корневую папку проекта, разработчик выполняет следующую последовательность команд:

    Команда cmake .. запускает этап конфигурации. Две точки указывают утилите, что файл CMakeLists.txt находится на один уровень выше текущей директории (то есть в корне проекта). На этом этапе анализируется система, ищутся компиляторы и создаются файлы для нативной системы сборки.

    Команда cmake --build . запускает фактическую компиляцию. Точка означает, что сборку нужно произвести в текущей директории.

    Огромным преимуществом такого подхода является инкрементальная сборка. В промышленных проектах количество исходных файлов часто достигает значений . Если среднее время компиляции одного файла составляет 2 секунды, полная сборка с нуля займет более 30 минут. Однако CMake отслеживает изменения. Если разработчик модифицировал небольшое количество файлов, например , при повторном вызове cmake --build . будут перекомпилированы только они. Время обновления программы составит всего около 10 секунд, что экономит колоссальное количество ресурсов.

    Переменные и кэширование

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

    Существует два основных типа переменных:

    * Локальные переменные — существуют только в рамках текущего файла конфигурации и его поддиректорий. * Кэшированные переменные — сохраняются в специальном файле CMakeCache.txt внутри папки сборки и запоминают свое значение между запусками.

    > Кэш CMake работает как долговременная память проекта. Он запоминает пути к найденным библиотекам и выбранные настройки компилятора, чтобы не тратить время на их поиск при каждом новом запуске.

    Примером использования кэшированной переменной является выбор типа сборки. Разработчик может собирать программу для отладки (с сохранением отладочных символов) или для релиза (с максимальной оптимизацией скорости работы).

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

    Управление целями

    Современный подход к написанию конфигураций базируется на концепции целей (targets). Цель — это конечный или промежуточный продукт сборки. Это может быть исполняемый файл (программа, которую запускает пользователь) или библиотека (набор функций, который используется другими программами).

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

    В данном примере в одном проекте создаются две независимые программы. Для сервера строго требуется стандарт C++20, а для клиента достаточно C++17. Использование префикса target_ гарантирует, что настройки не перемешаются. Это особенно важно в крупных продуктах, где разные модули могут разрабатываться разными командами с использованием разных стандартов.

    Понимание этих базовых принципов — первый шаг к профессиональной разработке. Грамотно настроенный фундамент позволяет в будущем легко добавлять сторонние библиотеки, писать автоматические тесты и настраивать непрерывную интеграцию.