Cmake

FetchContent и ExternalProject: подключение внешних зависимостей без системного менеджера пакетов

Введение

Статья 578 разобрала find_package для уже установленных в системе библиотек — FetchContent и ExternalProject решают альтернативную задачу: автоматическое скачивание и сборку зависимости прямо в процессе конфигурации/сборки текущего проекта, без необходимости, чтобы пользователь заранее самостоятельно устанавливал эту зависимость через системный пакетный менеджер.

Концепция

FetchContent скачивает исходный код зависимости (обычно из Git-репозитория, статья основного цикла про систему контроля версий) и включает её непосредственно в процесс конфигурации текущего проекта, как если бы её CMakeLists.txt был добавлен через add_subdirectory — это даёт зависимости тот же уровень интеграции с целями текущего проекта (видимость, статья 575), что и для собственных, локальных подмодулей проекта. ExternalProject — более старый, но в некоторых случаях более гибкий механизм, выполняющий сборку зависимости как полностью ОТДЕЛЬНЫЙ шаг (со своей собственной, независимой конфигурацией и сборкой), результат которой (скомпилированные библиотеки) затем используется текущим проектом — это даёт больше изоляции (зависимость собирается с собственными, потенциально отличающимися настройками компилятора), но за счёт меньшей интеграции и более сложного, многошагового процесса по сравнению с FetchContent.

Пример кода: FetchContent и ExternalProject

# FetchContent — современный, рекомендуемый подход для большинства случаев
include(FetchContent)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG v1.14.0 # ОБЯЗАТЕЛЬНО фиксированная версия — НЕ "main"/"master" для воспроизводимости сборки
)

FetchContent_MakeAvailable(googletest) # скачивание (если ещё не скачано) И конфигурация — за ОДИН вызов

add_executable(MyTests test_main.cpp)
target_link_libraries(MyTests PRIVATE gtest gtest_main) # цели googletest доступны, как обычные ЛОКАЛЬНЫЕ цели
# FetchContent с проверкой — использовать уже установленную в системе версию, если доступна
include(FetchContent)

find_package(fmt QUIET) # сначала пробуем найти УЖЕ установленную в системе библиотеку

if(NOT fmt_FOUND)
    FetchContent_Declare(fmt GIT_REPOSITORY https://github.com/fmtlib/fmt.git GIT_TAG 10.1.1)
    FetchContent_MakeAvailable(fmt) # скачиваем И собираем ТОЛЬКО если не нашли в системе
endif()
# ExternalProject — изоляция сборки зависимости с собственными настройками
include(ExternalProject)

ExternalProject_Add(MyDependency
    GIT_REPOSITORY https://github.com/example/mydependency.git
    GIT_TAG v2.0.0
    CMAKE_ARGS -DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF # СВОИ, отдельные настройки сборки
    INSTALL_COMMAND "" # пропуск шага установки, если он не нужен для конкретного случая использования
)

Пояснения к коду

FetchContent_Declare/FetchContent_MakeAvailable для googletest показывают типичный, идиоматичный современный паттерн подключения тестового фреймворка (основной цикл статей про тестирование) — после успешного выполнения этих команд цели gtest/gtest_main доступны для использования в target_link_libraries точно так же, как если бы они были собственными, локально написанными частями текущего проекта, без необходимости в отдельной, предварительной установке этой зависимости в системе. Явная фиксация GIT_TAG v1.14.0 (конкретной версии, а не «плавающей» ветки) — критично важная деталь для воспроизводимости сборки (аналогично общим принципам управления зависимостями из основного цикла статей), гарантирующая, что повторная сборка проекта спустя время использует ровно ту же версию зависимости, а не потенциально изменившееся содержимое ветки. Комбинация find_package(... QUIET) с условным FetchContent показывает практичный гибридный подход — предпочтение уже установленной в системе версии библиотеки (что может быть быстрее при повторных сборках и предпочтительнее с точки зрения управления версиями на уровне всей системы), с автоматическим запасным вариантом скачивания и локальной сборки, если подходящая версия не найдена в системе.

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

  • Использование «плавающей» ссылки на ветку (main, master) вместо конкретного, зафиксированного тега или коммита для GIT_TAG — это прямо нарушает воспроизводимость сборки (статья 446 DevOps-цикла про важность детерминированности CI), поскольку содержимое такой ветки может измениться между разными запусками конфигурации проекта в разное время, потенциально привнося неожиданные, не связанные с реальными изменениями в собственном проекте проблемы из-за изменившейся версии зависимости.
  • Игнорирование значительного увеличения времени первой конфигурации проекта из-за необходимости скачивания и сборки зависимостей через FetchContent — особенно для крупных зависимостей (или при медленном сетевом соединении), это может существенно замедлить первоначальную настройку окружения разработки или CI-пайплайна (статья 446), и для критичных по времени CI-сценариев стоит рассмотреть кеширование уже скачанных и собранных зависимостей между запусками (статья 446 DevOps-цикла про кеширование в CI), а не повторное скачивание и сборку при каждом отдельном запуске.
  • Конфликт версий одной и той же транзитивной зависимости, подключаемой через FetchContent несколькими разными частями одного большого проекта независимо — если два разных модуля проекта объявляют FetchContent_Declare для одной и той же библиотеки, но с разными версиями (GIT_TAG), реальное поведение CMake (обычно — используется первая успешно объявленная версия, последующие объявления той же зависимости игнорируются) может быть не вполне очевидным, и для крупных, многомодульных проектов стоит явно координировать версии общих зависимостей, объявляемых через FetchContent в разных частях проекта.
  • Использование ExternalProject_Add там, где значительно более простой FetchContent был бы достаточен — ExternalProject добавляет существенную дополнительную сложность (отдельный шаг сборки, более сложная передача результата обратно в основной проект) ценой большей изоляции, и эта дополнительная сложность оправдана лишь для случаев, где реально нужна именно эта изоляция (зависимость, требующая принципиально иных настроек сборки, не совместимых с настройками основного проекта, или зависимость, не основанная на CMake вовсе и требующая особого процесса сборки) — для большинства типичных, основанных на CMake зависимостей FetchContent обычно является более простым и предпочтительным выбором.