JavaRush /Курси /C++ SELF /Підключення залежностей через FetchContent

Підключення залежностей через FetchContent

C++ SELF
Рівень 32 , Лекція 3
Відкрита

1. Вступ

Коли ви пишете навчальні програми з 1–2 файлів, може здаватися, що зовнішні бібліотеки — це щось зі світу «великих і серйозних» розробників, які сидять у хмарочосах і пʼють каву з CI/CD-пайплайна. Але реальність простіша: щойно проєкт стає бодай трохи корисним, майже неминуче хочеться взяти готову цеглинку — для форматування, парсингу, логування, тестів чи роботи з конфігами, — замість того щоб писати все самотужки, а потім героїчно налагоджувати це три тижні.

Проблема в тому, що «взяти бібліотеку» в C++ — це не одна кнопка Install, як часто буває в інших екосистемах. Світ C++ історично дуже різноманітний: бібліотеки бувають header-only, бувають із бінарниками, бувають «CMake-friendly», а бувають і такі, де є Makefile, але він «налаштований під мого кота та Ubuntu 14.04». Тому нам потрібен принаймні один акуратний, зрозумілий і відтворюваний спосіб підключати залежності — і FetchContent якраз про це.

Три способи підключення залежностей

Якщо ви ще ніколи не підключали зовнішні бібліотеки, зазвичай у голові зʼявляються три інстинктивні варіанти. Перший — «скопіюю вихідні коди бібліотеки в папку third_party/ і забуду». Другий — «нехай бібліотека буде встановлена в системі, а я якось її знайду». Третій — «CMake, зроби красиво, будь ласка».

Порівняймо це в невеликій таблиці — не як «істину в останній інстанції», а як орієнтир для новачка:

Підхід Як виглядає Плюси Мінуси
«Копіпаста в проєкт» кладемо код залежності в репозиторій просто, працює офлайн боляче оновлювати, легко влаштувати кашу з ліцензій і версій
«Системне встановлення» бібліотека встановлена в ОС, проєкт її використовує швидко працює «тут і зараз» на одній машині на іншій машині «не збирається», версії плавають
FetchContent
CMake сам завантажує вихідні коди залежності в каталог збирання і підключає їх як частину збирання відтворювано, зручно, target-орієнтовано потрібна мережа (зазвичай), важливо фіксувати версії

Ось ключова думка сьогоднішньої лекції: FetchContent — це спосіб зробити так, щоб ваш проєкт сам «приносив із собою» потрібні вихідні коди залежності, зазвичай під час конфігурації CMake, а не покладався на магію середовища.

Що таке FetchContent

Уявіть, що ваш проєкт — це кухня, а CMake — шеф, який готує страву «зібрати застосунок». У вас є рецепт — ваш CMakeLists.txt, — але частина інгредієнтів не лежить у «холодильнику» проєкту: наприклад, потрібна «приправа fmt» — бібліотека форматування. FetchContent — це механізм, який каже шефу: «Якщо приправи немає, сходи в магазин за ось цією адресою, візьми ось цю конкретну упаковку, тобто версію, принеси її й використай під час готування».

Технічно документація CMake описує FetchContent як модуль, що вміє підготувати вміст, тобто завантажити або інакше отримати вихідні коди залежності, а потім, у типовому сценарії, додати цей проєкт до основного збирання так, щоб його таргети стали доступними для лінкування. Саме це й робить команда FetchContent_MakeAvailable().

Корисно памʼятати одну річ: FetchContent не «встановлює бібліотеку в систему» і не робить її глобально доступною. Він робить її частиною конкретного збирання в конкретному каталозі збирання. Тому out-of-source збирання й пресети, які ви вивчаєте зараз, дуже природно «дружать» із FetchContent: у кожному каталозі збирання у вас буде свій стан, зокрема й завантажені залежності.

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) гарантує, що залежність буде підготовлена і, якщо це можливо, додана до вашого збирання так, щоб таргети бібліотеки стали доступними в 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, який створює таргети, і його зручно лінкувати.

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. Ми просто лінкуємо таргет budgetbook із таргетом fmt::fmt. Ідея таргетів тут працює як LEGO: якщо бібліотека fmt коректно описала себе в CMake, вона сама «принесе» потрібні include-шляхи й прапорці туди, куди треба.

FetchContent_MakeAvailable() саме й робить так, щоб таргети залежності стали видимими для основного збирання.

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 той самий, що й у хеша коміту: ви фіксуєте «ось цей байт у байт архів», а не «що там зараз віддають за посиланням».

7. Що відбувається під час конфігурації

Щоб не сприймати FetchContent як магію, корисно тримати в голові послідовність подій. У спрощеному вигляді вона така:

flowchart TD
    A[Ви запускаєте конфігурацію CMake] --> B[FetchContent_Declare: записали правила]
    B --> C[FetchContent_MakeAvailable: перевірили, чи є вихідні коди залежності]
    C --> D{Залежність уже завантажена в каталозі збирання?}
    D -->|так| E[Використовуємо кешований вміст]
    D -->|ні| F[Завантажуємо або розпаковуємо залежність]
    E --> G["Додаємо залежність до збирання (як підпроєкт)"]
    F --> G
    G --> H[Таргети залежності доступні для target_link_libraries]

Ця модель добре пояснює два «побутові» спостереження.

Перше: чому out-of-source збирання важливе. Бо залежності й їхній «завантажений стан» живуть усередині каталогу збирання, а не бруднять вихідні коди.

Друге: чому зміна каталогу збирання або пресета часто призводить до повторного завантаження залежностей. Для 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». Тому, якщо ви оголосили залежність, але не зробили її доступною, таргети бібліотеки в target_link_libraries просто не зʼявляться.

Помилка № 3: викликати FetchContent_MakeAvailable() надто рано, а потім намагатися «перевизначити» залежність.
У великих проєктах залежності можуть оголошуватися в підпроєктах. Документація рекомендує спочатку оголосити всі деталі залежностей, а вже потім викликати MakeAvailable, інакше ваш пізній FetchContent_Declare(other ...) може бути проігнорований. У маленькому проєкті це теж корисна дисципліна: блок «усі залежності тут», а нижче — «підключаємо».

Помилка № 4: намагатися підключити бібліотеку «через include-папки», ігноруючи таргети.
Після FetchContent_MakeAvailable() правильний стиль — лінкуватися до таргета залежності, наприклад fmt::fmt, а не вручну прописувати include-директорії й намагатися вгадати, які прапорці потрібні. Інакше ви самі створюєте собі роботу замість CMake: замість того щоб використати готову модель «залежність як таргет», ви повертаєтеся до ручного збирання з 2005 року.

Помилка № 5: очікувати, що залежність стане «системно встановленою» і доступною всім проєктам.
FetchContent працює на рівні конкретного збирання: він складає вихідні коди й артефакти туди, де збирається ваш проєкт. Це не apt, не brew і не «глобальне встановлення». Тому не варто розраховувати, що інший проєкт на вашому компʼютері автоматично побачить ці бібліотеки: у нього буде свій каталог збирання і свій стан.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ