1. Введение
Когда вы пишете учебные программы из 1–2 файлов, кажется, что внешние библиотеки — это что-то из мира «больших и серьёзных» разработчиков, которые живут в небоскрёбах и пьют кофе из CI/CD‑пайплайна. Но реальность проще: как только проект становится чуть полезнее, вам почти неизбежно хочется взять готовый кирпичик — форматирование, парсер, логирование, тестовую библиотеку, работу с конфигами — вместо того чтобы писать всё самому и затем героически отлаживать это три недели.
Проблема в том, что «взять библиотеку» в C++ — это не одна кнопка Install, как часто бывает в других экосистемах. В C++ мир исторически разнообразный: библиотеки бывают header‑only, бывают с бинарниками, бывают «CMake‑friendly», а бывают такие, что у них есть Makefile, но он «настроен под моего кота и Ubuntu 14.04». Поэтому нам нужен хотя бы один аккуратный, понятный и воспроизводимый способ подключать зависимости — и FetchContent как раз про это.
Три способа подключать зависимости
Если вы никогда не подключали внешние библиотеки, то у вас обычно появляется три инстинктивных варианта. Первый — «скопирую исходники библиотеки в папку third_party/ и забуду». Второй — «пусть библиотека будет установлена в системе, а я как-нибудь её найду». Третий — «CMake, сделай красиво, пожалуйста».
Сравним это в небольшой таблице — не как «истину», а как навигацию для новичка:
| Подход | Как выглядит | Плюсы | Минусы |
|---|---|---|---|
| «Копипаста в проект» | кладём код зависимости в репозиторий | просто, работает офлайн | обновлять больно, легко устроить кашу из лицензий/версий |
| «Системная установка» | библиотека установлена в ОС, проект её использует | быстро вживую на одной машине | на другой машине «не собирается», версии плавают |
|
CMake сам скачивает исходники зависимости в build‑директорию и подключает как часть сборки | воспроизводимо, удобно, target‑ориентированно | требуется сеть (обычно), важно фиксировать версии |
И вот здесь возникает ключевая мысль сегодняшней лекции: FetchContent — это способ сделать так, чтобы ваш проект сам «приносил с собой» нужные исходники зависимости (обычно во время конфигурации CMake), а не надеялся на магию окружения.
Что такое FetchContent
Представьте, что ваш проект — это кухня, а CMake — шеф, который готовит блюдо «собрать приложение». У вас есть рецепт (ваш CMakeLists.txt), но часть ингредиентов не лежит в холодильнике проекта: например, нужна «приправа fmt» (библиотека форматирования). FetchContent — это механизм, который говорит шефу: «Если приправы нет, сходи в магазин по вот этому адресу, возьми вот эту конкретную упаковку (версию), принеси, и используй при готовке».
Технически CMake‑документация описывает FetchContent как модуль, который умеет “populate” контент (скачать/подготовить исходники зависимости) и затем (в типичном сценарии) добавить этот проект в основную сборку так, чтобы его targets стали доступны для линковки. Это как раз делает команда FetchContent_MakeAvailable().
Полезно помнить одну вещь: FetchContent не «ставит библиотеку в систему» и не делает её глобальной. Он делает её частью конкретной сборки в конкретной build‑директории. Поэтому out-of-source сборка и пресеты (которые вы изучаете в этот день) очень естественно дружат с FetchContent: в каждой build‑папке у вас будет своё состояние, включая скачанные зависимости.
2. Базовый паттерн FetchContent
Минимальный шаблон: Declare → MakeAvailable
Сейчас будет самый важный кусок: базовый «скелет» подключения зависимости. В современном CMake (начиная с 3.14) есть удобная команда FetchContent_MakeAvailable(), и документация прямо говорит: где возможно — лучше использовать её, а не вручную «популировать и add_subdirectory».
Минимальный паттерн выглядит так:
include(FetchContent)
FetchContent_Declare(
someLib
GIT_REPOSITORY https://example.com/someLib.git
GIT_TAG v1.2.3
)
FetchContent_MakeAvailable(someLib)
Смысл по шагам такой.
- Сначала include(FetchContent) подключает модуль и «открывает» вам команды FetchContent_Declare, FetchContent_MakeAvailable и (опционально) более низкоуровневые команды.
- Потом FetchContent_Declare(...) записывает «рецепт», откуда брать зависимость и какую именно версию. Важно, что Declare сам по себе ещё ничего не скачивает: он только фиксирует параметры.
- Потом FetchContent_MakeAvailable(name) гарантирует, что зависимость будет подготовлена (populated) и, если это возможно, добавлена в вашу сборку так, что targets библиотеки станут доступны в target_link_libraries.
Документация отдельно отмечает, что FetchContent_MakeAvailable() — «New in version 3.14». Это хороший ориентир: если ваш cmake_minimum_required() ниже, вам придётся использовать более старый (и более многословный) паттерн.
Дисциплина: сначала Declare, потом MakeAvailable
Здесь многие новички попадают в ловушку, потому что CMake выглядит как «просто скрипт»: написал строчку — она и выполнилась. Но с FetchContent есть важный нюанс: в больших деревьях проектов зависимости могут быть под‑зависимостями друг друга.
В документации есть прямое предупреждение в духе: проекту стоит объявить детали всех зависимостей до того, как он вызовет FetchContent_MakeAvailable() для любой из них — чтобы главный проект сохранял контроль над тем, какие именно версии будут использованы. Там же показан пример «WRONG / CORRECT», где поздний FetchContent_Declare(other ...) может быть проигнорирован, потому что другой проект уже успел объявить other раньше.
На человеческом языке это означает следующее: если вы хотите быть «главным взрослым» в своей сборке, то не вызывайте MakeAvailable сразу после каждого Declare. В идеале вы сначала делаете блок объявлений, а потом одним местом «подключаете» нужное.
Даже в маленьком проекте это дисциплинирует: вы одним взглядом видите список зависимостей и их версии.
5. Пример: подключаем fmt к приложению
Чтобы пример был живым, давайте продолжим наше учебное консольное приложение. Пусть оно называется BudgetBook — простой трекер расходов в консоли: читает команды, печатает баланс, добавляет операции. По функциональности оно пока простое, зато это удобно: мы не меняем архитектуру, а демонстрируем именно подключение зависимости.
Допустим, нам захотелось печатать сообщения чуть аккуратнее, чем через std::cout, и мы решили (просто как пример зависимости) использовать библиотеку форматирования fmt. Смысл не в том, что «вам срочно нужен fmt», а в том, что это типичный CMake‑дружелюбный проект, который создаёт target’ы, и его удобно линковать.
CMakeLists.txt: подключение fmt через FetchContent
cmake_minimum_required(VERSION 3.14)
project(BudgetBook LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(budgetbook
src/main.cpp
)
include(FetchContent)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
# ВАЖНО: в реальном проекте версию фиксируем (об этом — в следующем разделе)
GIT_TAG 9.1.0
)
FetchContent_MakeAvailable(fmt)
target_link_libraries(budgetbook PRIVATE fmt::fmt)
Обратите внимание на главный «target‑стиль» момент: мы не добавляем глобальные include‑директории и не пишем вручную пути к заголовкам fmt. Мы просто линкуем target budgetbook с target’ом fmt::fmt. Идея «целей» здесь работает как LEGO: если библиотека fmt корректно описала себя в CMake, то она сама «принесёт» нужные include‑пути и флаги туда, куда надо.
FetchContent_MakeAvailable() как раз и делает так, что targets зависимости становятся видны основной сборке.
main.cpp: минимально используем библиотеку
Сделаем крошечный пример. Мы не изучаем fmt как библиотеку — мы лишь показываем, что зависимость реально подключилась и работает.
// src/main.cpp
#include <string>
#include <vector>
// Внешняя библиотека (пришла через FetchContent)
#include <fmt/core.h>
int main() {
const std::string appName = "BudgetBook";
const int year = 2026;
fmt::print("{}: запускаемся, год {}\n", appName, year); // BudgetBook: запускаемся, год 2026
const std::vector<int> expenses = {10, 25, 7};
fmt::print("Операций: {}\n", expenses.size()); // Операций: 3
}
Если проект собрался и вывел строки, значит «провод» от FetchContent до вашего main.cpp подключён правильно: CMake скачал зависимость, добавил её в сборку, и затем линковщик увидел нужную библиотеку.
6. Фиксация версии: почему ветка master — это лотерея
Самое важное правило сегодняшней лекции звучит скучно, но спасает нервы: зависимости нужно фиксировать по версии.
Когда вы пишете:
GIT_TAG master
или
GIT_TAG main
вы, по сути, говорите: «каждый раз бери то, что сейчас лежит на кончике ветки». Сегодня оно собирается, завтра автор библиотеки сделал релиз, послезавтра поменял API, а через неделю вы внезапно получаете ошибку компиляции, хотя «я же ничего не менял!». Меняли не вы — менялся внешний мир. А внешний мир, как известно, любит сюрпризы.
CMake‑документация отдельно рекомендует: если контент скачивается с удалённого сервера, который вы не контролируете, лучше использовать хэш коммита для GIT_TAG, а не имя ветки или тега. Там прямо сказано, что commit hash более безопасен и помогает убедиться, что скачано именно то, что вы ожидали.
Тег релиза или хэш коммита
Есть два популярных варианта фиксации версии.
Если вы доверяете семантике релизов библиотеки и хотите читабельности:
GIT_TAG 9.1.0
Если вам важнее «железобетонная воспроизводимость» (особенно для учебных/командных проектов), используйте хэш коммита:
GIT_TAG 703bd9caab50b139428cea1aaff9974ebee5742e
В документации FetchContent такие примеры с commit hash встречаются прямо в тексте.
Практическая разница такая: тег теоретически могут «перепривязать» (редко, но возможно), а хэш коммита однозначно указывает на конкретное состояние репозитория. То есть «вчера, сегодня и через год» вы получите один и тот же код зависимости.
Архив: URL + URL_HASH
Иногда библиотека распространяется не через git, а через архив. Тогда FetchContent_Declare поддерживает вариант URL и хэш архива.
В документации есть пример вида:
FetchContent_Declare(
myCompanyIcons
URL https://.../iconset_1.12.tar.gz
URL_HASH MD5=...
)
Смысл URL_HASH тот же, что и у commit hash: вы фиксируете «вот этот байт‑в‑байт архив», а не «что там сейчас отдают по ссылке».
7. Что происходит во время конфигурации
Чтобы не воспринимать FetchContent как магию, полезно держать в голове последовательность событий. На упрощённом уровне это выглядит так:
flowchart TD
A[Вы запускаете CMake configure] --> B[FetchContent_Declare: записали правила]
B --> C[FetchContent_MakeAvailable: проверили, есть ли исходники зависимости]
C --> D{Зависимость уже скачана в build-директории?}
D -->|да| E[Используем кешированное содержимое]
D -->|нет| F[Скачиваем/распаковываем зависимость]
E --> G["Добавляем зависимость в сборку (как под-проект)"]
F --> G
G --> H[Targets зависимости доступны для target_link_libraries]
Эта модель хорошо объясняет два «бытовых» наблюдения.
Первое: почему out-of-source сборка важна. Потому что зависимости и их «скачанное состояние» живут внутри build‑директории, а не пачкают исходники.
Второе: почему смена build‑директории (или пресета) часто приводит к повторной загрузке зависимостей. Для CMake это действительно другое «состояние мира».
Коллизии имён и правило “first to record wins”
Есть ещё один нюанс, который кажется мелочью, пока вы не увидите его вживую. В FetchContent имя зависимости — это не просто комментарий для человека. По этому имени CMake решает, объявлялась ли зависимость уже раньше, и не объявлять ли её повторно.
В документации прямо сказано, что FetchContent_Declare() использует подход «first to record, wins»: если детали уже были записаны раньше в проекте (в том числе где-то в под‑проектах), то повторные объявления игнорируются.
Отсюда мораль: называйте зависимости «нормально и ожидаемо» (обычно официальным именем проекта), иначе вы можете случайно скачать одно и то же два раза под разными именами. А ещё важнее — не пытайтесь «переобъявить» зависимость поздно: скорее всего, вы опоздали, и ваше объявление проигнорируют.
8. Типичные ошибки
Ошибка №1: использовать GIT_TAG main/master и удивляться, что «вчера работало, сегодня нет».
Это классическая ловушка новичка: кажется, что вы «ничего не меняли», но зависимость подтянулась в новом состоянии. В результате проект становится невоспроизводимым. Лучше фиксировать версию либо тегом релиза, либо хэшем коммита; документация FetchContent отдельно подчёркивает, что commit hash безопаснее, когда сервер не под вашим контролем.
Ошибка №2: думать, что FetchContent_Declare() уже скачал библиотеку.
Declare — это запись параметров, а не «скачивание прямо сейчас». Загрузка и подключение происходят, когда вы вызываете FetchContent_MakeAvailable() (или вручную делаете «populate»). Поэтому, если вы объявили зависимость, но не сделали её доступной, targets библиотеки в target_link_libraries просто не появятся.
Ошибка №3: вызывать FetchContent_MakeAvailable() слишком рано, а потом пытаться «переопределить» зависимость.
В больших проектах зависимости могут объявляться в под‑проектах. Документация рекомендует сначала объявить все детали зависимостей, а уже потом вызывать MakeAvailable, иначе ваш поздний FetchContent_Declare(other ...) может быть проигнорирован. В маленьком проекте это тоже полезная дисциплина: блок «все зависимости здесь», а ниже «подключаем».
Ошибка №4: пытаться подключить библиотеку «через include‑папки», игнорируя targets.
После FetchContent_MakeAvailable() правильный стиль — линковаться к target’у зависимости (например, fmt::fmt), а не вручную прописывать include‑директории и пытаться угадать, какие флаги нужны. Иначе вы сами себе устраиваете работу CMake: вместо того чтобы использовать готовую модель «зависимость как target», вы возвращаетесь к ручной сборке из 2005 года.
Ошибка №5: ожидать, что зависимость станет «системно установленной» и доступной всем проектам.
FetchContent работает на уровне конкретной сборки: он складывает исходники и артефакты туда, где собирается ваш проект. Это не apt, не brew и не «глобальная установка». Поэтому не стоит рассчитывать, что другой проект на вашем компьютере автоматически увидит эти библиотеки: у него будет своя build‑директория и своё состояние.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ