CMakeSeniorExperience

Какая mental model CMake важна для диагностики: lifecycle, graph, runtime, targets, protocol, threads или resources?

Критическая mental model — граф targets плюс двухфазное выполнение (configure vs generate): понимание направленного графа зависимостей между targets и разницы между configure-фазой и generate-фазой (где работают генераторные выражения) объясняет большинство нетривиальных ошибок CMake.

Ключевая mental model для диагностики CMake: граф targets и двухфазное выполнение

Из перечисленных концепций наиболее универсальна для диагностики комбинация graph + lifecycle: понимание того, что CMake работает в две принципиально разные фазы (configure и build/generate), а между targets существует направленный граф зависимостей — объясняет большинство нетривиальных ошибок.

Две фазы и почему это важно

Configure phase — CMake читает CMakeLists.txt сверху вниз, выполняет все команды, строит внутреннее представление targets и их свойств, записывает CMakeCache.txt. Generate phase — создаёт нативные файлы сборки (Makefile, .sln, build.ninja).

Генераторные выражения ($<...>) вычисляются только на второй фазе — это ломает интуицию:

# НЕПРАВИЛЬНО: message() выполняется на configure phase, GE ещё не вычислен
message(STATUS "Output: $<TARGET_FILE:mylib>")  # выведет буквально $<TARGET_FILE:mylib>

# ПРАВИЛЬНО: использовать на этапе, где GE работает
add_custom_command(TARGET myapp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E echo "Built: $<TARGET_FILE:myapp>")

Граф targets: направленные зависимости

Каждый add_library() / add_executable() создаёт узел. target_link_libraries(A PRIVATE B) добавляет ребро A→B. CMake строит полный граф и вычисляет транзитивные свойства по нему.

add_library(base STATIC base.cpp)
target_include_directories(base PUBLIC include/)

add_library(network STATIC network.cpp)
target_link_libraries(network PUBLIC base)  # network экспортирует include/ base

add_executable(server server.cpp)
target_link_libraries(server PRIVATE network)  # server получает include/ base транзитивно

Диагностика: если include не видны в server, нужно смотреть на ребро PUBLIC/PRIVATE — не PRIVATE ли стоит там, где должно быть PUBLIC.

Targets vs переменные

Старый API CMake (include_directories(), link_libraries()) работает с глобальными переменными — они «протекают» в все targets ниже по файлу. Новый API (target_*()) привязан к конкретному target-узлу графа. Смешивание двух стилей даёт неожиданные результаты:

# Плохо: include_directories глобален — загрязняет все targets в этой области
include_directories(${OPENSSL_INCLUDE_DIR})

# Хорошо: только my_lib и его PUBLIC-потребители видят этот путь
target_include_directories(my_lib PUBLIC ${OPENSSL_INCLUDE_DIR})

Диагностические инструменты, опирающиеся на эту model

# Распечатать свойства конкретного target
cmake --build build --target help         # список всех targets
cmake -B build --graphviz=deps.dot        # визуализировать граф зависимостей
dot -Tpng deps.dot -o deps.png

# Вывести все свойства target (требует CMake 3.19+)
cmake -B build && cmake --install build --dry-run
# В CMakeLists.txt для отладки
get_target_property(INC_DIRS my_lib INTERFACE_INCLUDE_DIRECTORIES)
message(STATUS "Interface includes: ${INC_DIRS}")

get_target_property(LINK_LIBS server LINK_LIBRARIES)
message(STATUS "Link libraries: ${LINK_LIBS}")

Lifecycle конкретного target

Понимание жизненного цикла target позволяет корректно расставить команды:

  • add_library() — создание target
  • target_sources() — добавление исходников (можно вызывать в любом месте до generate)
  • target_compile_options/definitions/features() — свойства компиляции
  • target_link_libraries() — зависимости и линковка
  • install(TARGETS ...) — описание инсталляции

Подводные камни

  • Циклические зависимости. CMake не разрешает циклы в графе: A→B→A вызовет ошибку. Решение — выделить общий базовый target C.
  • INTERFACE vs PRIVATE vs PUBLIC. Ошибка в этом выборе приводит либо к «протечке» флагов, либо к отсутствию нужных include в потребителях.
  • configure_file() выполняется на configure phase. Изменение входного файла не пересобирает проект автоматически — нужно явно добавить зависимость через set_property(DIRECTORY ... ADDITIONAL_MAKE_CLEAN_FILES ...).
  • Imported targets не входят в граф install. Забытый install(IMPORTED_RUNTIME_ARTIFACTS ...) приводит к отсутствию DLL в пакете на Windows.
  • ALIAS targets — read-only. add_library(Foo::Foo ALIAS foo) нельзя использовать как аргумент target_link_libraries с нужным свойством PUBLIC — только как ссылку.
  • add_subdirectory() меняет текущий scope. Переменные, объявленные в поддиректории, не видны в родительском CMakeLists.txt без PARENT_SCOPE.
  • Порядок target_link_libraries имеет значение для статических библиотек. При circular dependencies между статическими либами (A ссылается на B, B на A) нужно дублировать: target_link_libraries(app A B A).

What hurts your answer

  • Сразу обвинять CMake, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг CMake
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics