JavaRush /Курсы /C++ SELF /CMake Presets: воспроизводимые пресеты

CMake Presets: воспроизводимые пресеты

C++ SELF
32 уровень , 2 лекция
Открыта

1. Что такое Presets и где они живут

Если вы когда-нибудь слышали фразу «у меня на компьютере работает», то вы уже интуитивно понимаете, зачем придумали CMake Presets. Проблема не в том, что кто-то плохой программист, а в том, что настройки сборки часто живут в десятке мест: в IDE, в переменных окружения, в кэше CMake, в разных build‑папках и в головах участников команды (самое нестабильное хранилище из всех).

Представим ситуацию. Вы в CLion выбрали Debug, у друга в VS Code почему-то Release, а третий человек вообще собирает из консоли и однажды забыл указать стандарт C++23. Исходники одни и те же, а поведение «прыгает»: где-то assert сработал, где-то нет; где-то оптимизация переупорядочила код так, что отладчик показывает «переменная недоступна»; а где-то линковка внезапно подтянула другую версию библиотеки.

CMake Presets — это способ сказать: «Вот официальные сценарии сборки проекта. Они описаны в файле, лежат в репозитории, и мы все используем одно и то же». Не «на словах одно и то же», а реально одно и то же: одинаковые ключевые переменные конфигурации, одинаковые build‑директории, одинаковые команды.

Что такое preset: маленький JSON, который экономит большие нервы

Preset — это именованный сценарий для CMake. Причём сценарий обычно делится на несколько этапов: сначала мы конфигурируем проект (CMake генерирует файлы сборки в build‑папке), потом собираем, а иногда ещё и запускаем тесты. CMake Presets позволяют описать эти этапы так, чтобы вы (или ваш преподаватель, или CI) могли сказать: «Собери preset debug» — и не вспоминать, какие там флаги, пути и параметры.

Полезно держать в голове простую модель:

flowchart LR
    A["Исходники (sourceDir)"] --> B["Configure preset
cmake --preset ..."] B --> C["Build directory (binaryDir)
build-debug/ или build-release/"] C --> D["Build preset
cmake --build --preset ..."] C --> E["Test preset
ctest --preset ... (если нужно)"]

Здесь важная мысль: configure preset отвечает за то, как появляется build‑папка и что лежит в её CMakeCache.txt. build preset отвечает за сборку уже сконфигурированного каталога. test preset — за запуск тестов (если они есть).

Да, это ровно то, что обычно делала IDE «по кнопке». Разница в том, что теперь это не «настроено у вас где-то в интерфейсе», а описано в репозитории и повторяемо на любой машине.

CMakePresets.json: структура файла без мистики

CMake Presets описываются в JSON-файле CMakePresets.json в корне проекта (рядом с CMakeLists.txt). Это именно JSON, а значит он строгий: кавычки обязательны, запятые важны, комментарии в стиле // — нельзя (и да, это печально, потому что комментарии были бы удобны, но JSON суров).

Минимальная структура файла выглядит так:

{
  "version": 6,
  "configurePresets": [],
  "buildPresets": [],
  "testPresets": []
}

Поле version — это версия формата preset’ов (не версия CMake и не версия вашего проекта). На практике вы чаще всего увидите версии вроде 4/5/6: главное — чтобы ваш CMake умел её читать. Если у вас CMake старый и внезапно «не понимает» формат, это будет выглядеть как очень обидная ошибка ещё до сборки.

В этом файле вы обычно описываете «официальные» пресеты проекта: debug, release, возможно asan (но санитайзеры у нас будут отдельным днём), иногда ещё ci. Идея в том, чтобы не делать 20 вариантов «на всякий случай», а закрепить 2–4 рабочих сценария, которыми пользуются все.

3. Configure preset: build‑папка и ключевые переменные

Configure preset — это, по сути, ответ на вопрос: «Куда и с какими параметрами мы конфигурируем проект?». Он задаёт binaryDir, может задавать generator, и самое практичное — задаёт cacheVariables, то есть переменные, которые попадут в CMakeCache.txt и повлияют на конфигурацию.

Для начала сделаем базовый preset и от него унаследуем Debug и Release. Это не «красота ради красоты»: inheritance экономит копипаст и снижает шанс, что в одном пресете вы забыли включить стандарт C++23, а в другом — нет.

Пример:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "base",
      "hidden": true,
      "cacheVariables": {
        "CMAKE_CXX_STANDARD": "23"
      }
    },
    {
      "name": "debug",
      "inherits": "base",
      "binaryDir": "${sourceDir}/build-debug",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug"
      }
    },
    {
      "name": "release",
      "inherits": "base",
      "binaryDir": "${sourceDir}/build-release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release"
      }
    }
  ]
}

Здесь происходит несколько важных вещей.

binaryDir жёстко фиксирует, где будет жить сборка. Это дисциплина: Debug и Release не мешают друг другу, и у вас не будет ситуации «я поменял тип сборки, а оно как-то странно не применилось». На самом деле оно применилось — просто в кэше осталась старая конфигурация, и вы живёте в прошлом. Presets помогают не жить в прошлом.

${sourceDir} — переменная CMake Presets, которая означает «директория с исходниками». Это лучше, чем писать абсолютные пути вроде C:\Users\Alice\Projects\..., потому что абсолютные пути делают preset непереносимым: на чужой машине он превратится в тыкву.

cacheVariables — это именно то, что попадёт в кэш. Самый популярный пример дня — CMAKE_BUILD_TYPE, который задаёт Debug/Release (в одноконфигурационных генераторах). Ещё один разумный пример — CMAKE_CXX_STANDARD, чтобы проект собирался в C++23 «по договору», а не «потому что у вас так настроено в IDE».

Чтобы связать это с темой Debug/Release, можно добавить в учебный проект микропроверку в коде и видеть, что реально собирается:

#include <iostream>

int main() {
#ifdef NDEBUG
    std::cout << "Release-like build (NDEBUG is defined)\n";
#else
    std::cout << "Debug-like build (NDEBUG is NOT defined)\n";
#endif
}

Смысл не в том, чтобы так писать «в реальном продукте», а в том, чтобы на практике почувствовать: preset меняет конфигурацию, конфигурация влияет на макросы (например, NDEBUG), а макросы влияют на компиляцию.

4. Build и Test presets: сборка и тесты как повторяемые шаги

Build preset: сборка — это отдельный шаг

Build preset отвечает за «как собрать то, что уже сконфигурировано». Это звучит скучно, пока вы не поймаете себя на том, что вы 15 раз подряд запускаете сборку из разных мест: то из IDE, то из терминала, то случайно из другой build‑папки, потому что вкладка терминала была не та.

Build preset обычно ссылается на configure preset по имени. Пример:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "debug",
      "binaryDir": "${sourceDir}/build-debug",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    }
  ],
  "buildPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    }
  ]
}

Смысл такой: команда сборки «знает», какой build‑каталог использовать, и вам не нужно вспоминать, где он лежит. Это особенно удобно, когда build‑папок несколько, и вы реально хотите, чтобы они были независимыми.

На практике build preset может содержать дополнительные параметры: например, конфигурацию (для multi-config генераторов), параллельность (сколько задач собирать одновременно), режим подробного вывода. Но на уровне нашего дня достаточно уловить: configure preset создаёт и настраивает build‑папку, build preset — собирает.

Test preset: запуск тестов тоже можно превратить в «договор»

Test preset — это способ так же зафиксировать запуск тестов. Здесь важно не путать: test preset — это не «фреймворк тестов» и не «как писать тесты». Это просто сценарий запуска ctest, который работает, если ваш проект уже добавил тесты в CMake (например, через enable_testing() и add_test(...)).

Да, полноценные unit‑тесты у нас будут сильно позже. Но именно сейчас полезно увидеть идею: если проект умеет тестироваться, то запуск тестов тоже должен быть воспроизводимым.

Минимальный пример test preset может выглядеть так:

{
  "version": 6,
  "configurePresets": [
    {
      "name": "debug",
      "binaryDir": "${sourceDir}/build-debug",
      "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug" }
    }
  ],
  "testPresets": [
    {
      "name": "debug",
      "configurePreset": "debug"
    }
  ]
}

Этот preset говорит: «Запускай тесты из build‑директории, которая соответствует debug». На практике это убирает массу путаницы, когда человек запускает ctest из build-release, а смотрит на результаты как будто это build-debug. В программировании и так достаточно загадок; не нужно добавлять ещё одну на ровном месте.

5. Использование в жизни: команды и разделение на общий/локальный файл

Три короткие команды вместо «а что я там накликал»

Presets особенно приятно раскрываются, когда вы используете их одинаково везде: на своей машине, у преподавателя, у друга по группе и в CI. Идея очень простая: вы больше не держите в голове «правильные команды», вы держите в голове имена пресетов.

Типичный цикл выглядит так:

cmake --preset debug
cmake --build --preset debug
ctest --preset debug

В этом месте мозг новичка обычно спрашивает: «А почему нельзя одной командой?» Можно, но не нужно. Разделение на configure/build/test — это не бюрократия, а отражение реальности: конфигурация создаёт проект сборки и кэширует параметры, сборка делает бинарники, тесты запускают проверки.

Если вы поменяли CMakePresets.json, важно помнить, что build‑каталог уже мог быть сконфигурирован со старыми значениями. Тогда нужно заново «сконфигурировать» preset (первой командой), чтобы изменения попали в кэш и генерацию. Presets не отменяют существование CMakeCache.txt; они просто делают его создание управляемым.

CMakePresets.json и CMakeUserPresets.json: что коммитим, а что оставляем себе

Очень типичная история: вы хотите, чтобы пресеты были общими, но при этом у каждого разработчика могут быть локальные особенности. Например, кто-то хочет включить очень подробный лог сборки, кто-то — указать путь к нестандартному компилятору, а кто-то — собрать в папку на другом диске, потому что на системном мало места.

Для этого в CMake существует идея двух файлов:

  • CMakePresets.jsonобщий, его обычно коммитят в репозиторий. Это «официальные» пресеты проекта.
  • CMakeUserPresets.jsonпользовательский, его обычно не коммитят. Это «мои локальные удобства», которые не должны ломать воспроизводимость у остальных.

Даже если вы пока работаете один, полезно привыкать к этой дисциплине: «общие» сценарии — в общий файл, «личные» — в пользовательский. Иначе через месяц вы обнаружите, что проект «на вашей машине собирается только потому, что вы когда-то руками добавили настройку в IDE», а больше никто не знает какую.

6. Типичные ошибки при работе с Presets

В этой теме ошибки обычно не «сложные», а коварно-бытовые: вы делаете всё почти правильно, но один маленький нюанс превращает сборку в лотерею. Хорошая новость в том, что эти грабли довольно предсказуемые, и если вы один раз их поймёте, вы будете узнавать их по звуку.

Ошибка №1: один binaryDir на все случаи жизни.
Если два пресета (например, Debug и Release) используют одну и ту же build‑папку, вы снова возвращаетесь в мир «почему настройки не применились». Смысл пресетов — в том числе дисциплина раздельных build‑каталогов, чтобы конфигурации не перетирали кэш друг друга. Делайте build-debug/ и build-release/ как минимум, и жизнь станет заметно спокойнее.

Ошибка №2: ожидание, что build preset «сам применит новые cacheVariables».
Build preset ничего не «настраивает», он просто запускает сборку в уже существующем build‑каталоге. Если вы изменили cacheVariables (например, добавили стандарт C++23 или поменяли build type), но не перезапустили configure step, то сборка продолжит жить со старым CMakeCache.txt. В этом нет магии: это просто состояние.

Ошибка №3: жёсткие абсолютные пути в пресетах.
Абсолютные пути делают пресеты «привязанными к вашему компьютеру». Сегодня вы рады, что быстро указали C:\..., а завтра вы не можете собрать проект ни на другой машине, ни в CI, ни даже у себя после переезда папки. Используйте ${sourceDir} и другие переменные, чтобы пресеты оставались переносимыми.

Ошибка №4: попытка сделать из preset’ов замену пользовательскому вводу.
Preset — это про сборку, а не про «параметры работы программы». Нельзя нормально передавать через пресеты вещи уровня «имя пользователя» или «путь к входному файлу» как будто это runtime‑настройки. Да, технически можно определить макрос и вплести его в код, но вы быстро придёте к проекту, который пересобирается ради смены одного значения. Presets — про то, как компилировать, а не про то, как пользоваться программой.

Ошибка №5: игнорирование того, что CMAKE_BUILD_TYPE не всегда работает так, как ожидается.
В некоторых средах CMake использует генераторы, где Debug/Release выбираются не через CMAKE_BUILD_TYPE на этапе конфигурации, а иначе (это уже детали конкретного генератора). На уровне нашей лекции важно запомнить практическое правило: если вы видите, что preset «Debug» почему-то даёт поведение как у Release, не надо спорить с реальностью — надо проверить, какой генератор использует ваша среда и действительно ли учитывается CMAKE_BUILD_TYPE.

1
Задача
C++ SELF, 32 уровень, 2 лекция
Недоступна
Первый пресет
Первый пресет
1
Задача
C++ SELF, 32 уровень, 2 лекция
Недоступна
Пресет сборки
Пресет сборки
1
Задача
C++ SELF, 32 уровень, 2 лекция
Недоступна
Два режима
Два режима
1
Задача
C++ SELF, 32 уровень, 2 лекция
Недоступна
Договор теста
Договор теста
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ