Введение
Статья 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обычно является более простым и предпочтительным выбором.