Cmake

Генераторные выражения (generator expressions): $<...>, условная конфигурация

Введение

Статья 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 описывает значительно более широкий набор доступных генераторных выражений, покрывающих многие практически важные случаи условной конфигурации сборки, и знание этого более широкого набора часто позволяет избежать более сложных, обходных решений для задач, для которых уже существует прямое, специально предназначенное генераторное выражение.