Введение
Статья 576 кратко использовала генераторное выражение $<$<CONFIG:Debug>:...> без подробного объяснения — в этой статье разберём этот специфичный, но мощный синтаксис CMake систематически: генераторные выражения вычисляются не на этапе обычного выполнения команд CMakeLists.txt, а позже, на этапе генерации файлов сборки, что позволяет выразить условия, зависящие от информации, недоступной раньше (как конкретная конфигурация сборки — Debug/Release).
Концепция
Обычные команды CMake (if/set) выполняются на этапе конфигурации, когда некоторая информация (например, какая именно конфигурация — Debug или Release — будет реально выбрана при сборке для генераторов, поддерживающих несколько конфигураций одновременно, как Visual Studio) ещё не определена окончательно — генераторные выражения (синтаксис $<...>) решают эту проблему, откладывая фактическое вычисление условия до момента генерации финальных файлов сборки, когда вся нужная информация уже доступна. Это особенно важно для многоконфигурационных генераторов (Visual Studio, Xcode), где, в отличие от Makefile/Ninja (требующих отдельной директории сборки для каждой конфигурации), одна и та же директория сборки поддерживает переключение между Debug/Release без повторной конфигурации, и обычный if(CMAKE_BUILD_TYPE STREQUAL "Debug") для таких генераторов не работает ожидаемым образом, поскольку конкретная конфигурация для таких генераторов реально выбирается позже, во время самой сборки, а не на этапе конфигурации.
Пример кода: Генераторные выражения (generator
# Генераторное выражение для конфигурации Debug/Release — работает КОРРЕКТНО для ЛЮБОГО генератора
target_compile_definitions(MyApp PRIVATE
$<$<CONFIG:Debug>:DEBUG_BUILD> # макрос DEBUG_BUILD только для Debug-конфигурации
$<$<CONFIG:Release>:NDEBUG_EXTRA> # другой макрос только для Release
)
# В отличие от менее надёжного, не работающего корректно для многоконфигурационных генераторов:
# if(CMAKE_BUILD_TYPE STREQUAL "Debug")
# target_compile_definitions(MyApp PRIVATE DEBUG_BUILD) # НЕ СРАБОТАЕТ как ожидается для Visual Studio!
# endif()
# Условная компиляция в зависимости от компилятора или платформы
target_compile_options(MyApp PRIVATE
$<$<CXX_COMPILER_ID:MSVC>:/W4> # флаги предупреждений для MSVC
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra> # флаги для GCC/Clang
)
target_link_libraries(MyApp PRIVATE
$<$<PLATFORM_ID:Windows>:ws2_32> # системная библиотека сокетов только для Windows
$<$<PLATFORM_ID:Linux>:pthread> # системная библиотека потоков только для Linux
)
# Генераторное выражение для свойств цели — путь к собранному артефакту
add_custom_command(TARGET MyApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "Собранный файл: $<TARGET_FILE:MyApp>"
# $<TARGET_FILE:MyApp> — РЕАЛЬНЫЙ путь к итоговому исполняемому файлу, известный только при генерации
)
Пояснения к коду
Сравнение генераторного выражения $<$<CONFIG:Debug>:DEBUG_BUILD> и закомментированной, менее надёжной альтернативы через обычный if(CMAKE_BUILD_TYPE STREQUAL "Debug") показывает суть проблемы, которую решают генераторные выражения — для однопроходных генераторов (Makefile, Ninja) CMAKE_BUILD_TYPE действительно известен уже на этапе конфигурации, и обычный if сработал бы корректно, но для многоконфигурационных генераторов (Visual Studio) этой информации ещё нет на этом этапе (пользователь выбирает конфигурацию позже, прямо в самой Visual Studio), и только генераторное выражение, вычисляемое именно в момент генерации файлов для каждой конкретной, реально выбранной конфигурации, работает корректно универсально для обоих типов генераторов. Условные флаги компилятора через $<CXX_COMPILER_ID:...> показывают практичное применение для платформенно/компиляторно-специфичных настроек, аналогично условной компиляции через препроцессор (статья 527 цикла синтаксиса C/C++), но выраженной на уровне самой системы сборки, а не внутри исходного кода. $<TARGET_FILE:MyApp> показывает другую категорию генераторных выражений — не условную логику, а получение информации о цели (точный путь к её итоговому артефакту сборки), которая тоже становится определённой только на этапе генерации, что особенно полезно для пост-сборочных команд (add_custom_command), нуждающихся в точном, реальном пути к только что собранному файлу.
Подводные камни
- Использование обычного
if(CMAKE_BUILD_TYPE STREQUAL "Debug")там, где реально нужно генераторное выражение, особенно если проект должен поддерживать и одноконфигурационные, и многоконфигурационные генераторы одновременно (для разных платформ разработки в одной команде) — как явно показано в примере, это работает не универсально, и код, протестированный только с Makefile/Ninja генератором (гдеCMAKE_BUILD_TYPEдействительно доступен на этапе конфигурации), может неожиданно не работать для разработчика, использующего Visual Studio, без какой-либо явной ошибки конфигурации, а просто молчаливо неприменённой логикой. - Сложность вложенных генераторных выражений, существенно снижающая читаемость
CMakeLists.txt— синтаксис$<условие:значение>для простых, единичных случаев читается приемлемо, но для более сложных, многократно вложенных условий (комбинации компилятора, платформы, конфигурации одновременно) итоговое выражение может стать труднопонятным, и для действительно сложной условной логики стоит рассмотреть разбиение на несколько более простых выражений или явные CMake-переменные с понятными именами, вычисляемые через обычныеifтам, где это применимо (для условий, реально известных уже на этапе конфигурации). - Попытка использовать генераторное выражение в контексте, где CMake ожидает обычное, немедленно вычисляемое значение (например, в условии обычного
if(), не предназначенного для отложенного вычисления) — генераторные выражения предназначены для использования именно в контекстах, явно поддерживающих их отложенное вычисление (большинствоtarget_*команд), и попытка использовать этот синтаксис там, где он не ожидается и не поддерживается, либо игнорируется, либо приводит к буквальной, не интерпретированной текстовой строке вместо ожидаемого условного вычисления. - Недооценка широты возможностей генераторных выражений, ограничиваясь только базовым
$<CONFIG:...>без знания других полезных категорий (информация о компиляторе, платформе, свойствах целей, как$<TARGET_FILE:...>) — полная документация CMake описывает значительно более широкий набор доступных генераторных выражений, покрывающих многие практически важные случаи условной конфигурации сборки, и знание этого более широкого набора часто позволяет избежать более сложных, обходных решений для задач, для которых уже существует прямое, специально предназначенное генераторное выражение.