Какая 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()— создание targettarget_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
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения