Підключення зовнішніх бібліотек: find_package, add_subdirectory, FetchContent, pkg-config
Підключення зовнішніх бібліотек: find_package, add_subdirectory, FetchContent, pkg-config
🎯 Мета лекції
- Назвати п'ять способів підключити зовнішню бібліотеку та обрати доречний для задачі.
- Користуватися
find_packageз компонентами та розуміти, що відбувається, коли пакет не знайдено. - Завантажувати залежності під час конфігурації за допомогою
FetchContent. - Підключати бібліотеки без CMake-підтримки вручну та через
pkg-config.
🔑 Ключові терміни
- Імпортована ціль (imported target): ціль на кшталт
Boost::filesystem, яку створюєfind_packageдля вже встановленої бібліотеки. - Режим Module / Config: два способи, якими
find_packageшукає бібліотеку. FetchContent: модуль для завантаження джерел залежності під час конфігурації.pkg-config: утиліта, яка зберігає прапорці збірки бібліотек в окремих.pcфайлах.
Власні бібліотеки вміємо збирати. Реальний проєкт використовує ще й чужі: JSON-парсер, фреймворк тестування, мережеву бібліотеку. Нижче розглянуто п'ять способів їх підключити. Вони розташовані від найпростішого до найбільш ручного.
Спосіб 1: find_package (бібліотека вже встановлена)
Команда find_package шукає на комп'ютері вже встановлену бібліотеку і створює для неї цілі, які можна передати в target_link_libraries.
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Шукаємо бібліотеку
find_package(Threads REQUIRED) # потоки POSIX
add_executable(MyApp main.cpp)
# Зв'язуємо
target_link_libraries(MyApp Threads::Threads)
Ключове слово REQUIRED означає: якщо пакет не знайдено, конфігурація зупиняється з помилкою. Без нього CMake піде далі, а наявність пакета треба перевіряти вручну змінною Threads_FOUND.
Рядок Found Threads: TRUE підтверджує успіх. Тепер ціль Threads::Threads можна лінкувати.
Як find_package шукає пакет
Існує два режими пошуку:
| Режим | Що шукає CMake | Хто пише цей файл |
|---|---|---|
| Module | Файл Find<Пакет>.cmake | Розробники CMake (для популярних бібліотек) або автор проєкту |
| Config | Файл <Пакет>Config.cmake або <пакет>-config.cmake | Автор самої бібліотеки, його кладе поруч з бібліотекою при інсталяції |
За замовчуванням CMake спершу пробує Module, потім Config. Режим Config можна зафіксувати словом CONFIG: find_package(fmt CONFIG REQUIRED). Така форма використовується з менеджером пакетів vcpkg і надійніша, бо не залежить від застарілих модулів Find*.cmake.
Де саме CMake шукає Config-файли, визначають шляхи системи та змінна CMAKE_PREFIX_PATH. Якщо бібліотека лежить у нестандартній папці, передайте її при конфігурації:
cmake -S . -B build -DCMAKE_PREFIX_PATH=/opt/mylibs
# або точно вказати папку з Config-файлом конкретного пакета
cmake -S . -B build -Dfmt_DIR=/opt/mylibs/lib/cmake/fmt
Приклад: OpenCV
cmake_minimum_required(VERSION 3.10)
project(OpenCVExample)
set(CMAKE_CXX_STANDARD 17)
# Шукаємо OpenCV
find_package(OpenCV REQUIRED)
# Виводимо інформацію
message(STATUS "OpenCV version: ${OpenCV_VERSION}")
message(STATUS "OpenCV include dirs: ${OpenCV_INCLUDE_DIRS}")
add_executable(MyApp main.cpp)
# Підключаємо заголовки та бібліотеки
target_include_directories(MyApp PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(MyApp ${OpenCV_LIBS})
Цей приклад показує старий стиль: пакет повертає змінні (OpenCV_INCLUDE_DIRS, OpenCV_LIBS), які доводиться передавати в дві різні команди. Сучасні пакети створюють цілі, і достатньо одного target_link_libraries, як у наступному прикладі.
Приклад: Boost із компонентами
cmake_minimum_required(VERSION 3.10)
project(BoostExample)
set(CMAKE_CXX_STANDARD 17)
# Шукаємо конкретні компоненти Boost
find_package(Boost 1.70 REQUIRED COMPONENTS filesystem system)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp
Boost::filesystem
Boost::system
)
Число 1.70 — мінімальна версія. Слово COMPONENTS перелічує бібліотеки Boost, які реально потрібні (Boost складається з десятків окремих частин, і більшість із них header-only).
Що бачимо, коли пакета немає
Якщо на комп'ютері Boost не встановлено, конфігурація зупиняється з помилкою:
У повідомленні перелічено, чого саме не вистачає. Виправити можна трьома способами: встановити бібліотеку (системним менеджером або vcpkg), вказати CMAKE_PREFIX_PATH або перейти на FetchContent.
FindBoost вважається застарілим (політика CMP0167): сучасний Boost постачає власні Config-файли. З політикою за замовчуванням і cmake_minimum_required(VERSION 3.10) CMake виводить попередження про це. У новому коді найкраще встановлювати Boost через vcpkg і викликати find_package(Boost CONFIG REQUIRED COMPONENTS ...).Спосіб 2: бібліотека в підпапці (add_subdirectory)
Якщо вихідний код бібліотеки лежить прямо у вашому репозиторії (скопійований або підключений як git submodule), її збирають разом з проєктом:
Команда add_subdirectory(шлях) виконує CMakeLists.txt з цієї папки, а цілі, створені там, стають доступними в усьому проєкті. Вся бібліотека збирається вашим компілятором і з вашими прапорцями, тому проблем із сумісністю версій компілятора немає. Платите місцем у репозиторії і тим, що бібліотека збирається щоразу разом із проєктом.
Спосіб 3: автоматичне завантаження (FetchContent)
Модуль FetchContent завантажує джерела залежності під час конфігурації, а далі діє як add_subdirectory. Ручне копіювання коду в репозиторій не потрібне.
cmake_minimum_required(VERSION 3.14)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
# Описуємо, звідки брати Google Test
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG release-1.12.1
)
# Завантажуємо та підключаємо
FetchContent_MakeAvailable(googletest)
add_executable(MyApp main.cpp)
# Ціль gtest_main тепер доступна
target_link_libraries(MyApp PRIVATE GTest::gtest_main)
Дві команди мають різну роль:
FetchContent_Declareлише описує джерело (git-репозиторій і мітку або URL архіву). Нічого не завантажується.FetchContent_MakeAvailableзавантажує джерело вbuild/_deps/, викликаєadd_subdirectoryдля нього і робить його цілі доступними.
GIT_TAG на конкретній версії (тег або хеш коміту), а не на main. Інакше збірка у вас і у колеги через місяць дасть різні результати, бо автор бібліотеки міг змінити API.Повний приклад із JSON-бібліотекою nlohmann/json
Завантажимо архів релізу бібліотеки. Для великих репозиторіїв архів швидший за клонування історії git:
Перший запуск конфігурації завантажує бібліотеку, наступні беруть її з build/_deps/:
age, city, name, skills), а не в порядку створення. Тип nlohmann::json за замовчуванням зберігає об'єкти в упорядкованому словнику. Якщо потрібен порядок вставки, використовують nlohmann::ordered_json.Обмеження FetchContent
- Повільна перша конфігурація. Бібліотека завантажується і збирається разом з проєктом. Для великих бібліотек (Boost, Qt) це недоцільно.
- Потрібен інтернет під час першої конфігурації. Рішенням для CI-систем є кеш
build/_deps/або менеджер пакетів із бінарним кешем. - Усі налаштування залежності змішуються з вашими. Змінні та опції бібліотеки з'являються у вашому кеші CMake, а помилка в чужому
CMakeLists.txtстає вашою.
З цих причин FetchContent найкраще підходить для невеликих бібліотек (JSON, форматування, тестові фреймворки). Для великих залежностей кращий менеджер пакетів, про який — окрема тема.
Спосіб 4: бібліотека без підтримки CMake (ручне підключення)
Деякі старі або внутрішні бібліотеки не мають ні Config-файлу, ні Find*.cmake. Тоді шляхи прописують руками:
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Вручну вказуємо шляхи
set(MYLIB_INCLUDE_DIR "C:/libs/mylib/include")
set(MYLIB_LIBRARY "C:/libs/mylib/lib/mylib.lib")
add_executable(MyApp main.cpp)
# Підключаємо
target_include_directories(MyApp PRIVATE ${MYLIB_INCLUDE_DIR})
target_link_libraries(MyApp ${MYLIB_LIBRARY})
Недолік очевидний: шлях C:/libs/... існує лише на вашому комп'ютері. Збірка на іншій машині або ОС зламається. Прийнятний варіант — зробити шляхи налаштовуваними:
# Значення за замовчуванням, яке користувач може перевизначити через -D
set(MYLIB_ROOT "" CACHE PATH "Папка, у якій встановлено mylib")
find_path(MYLIB_INCLUDE_DIR mylib.h HINTS "${MYLIB_ROOT}/include")
find_library(MYLIB_LIBRARY mylib HINTS "${MYLIB_ROOT}/lib")
if(NOT MYLIB_INCLUDE_DIR OR NOT MYLIB_LIBRARY)
message(FATAL_ERROR "mylib не знайдено. Вкажіть -DMYLIB_ROOT=...")
endif()
add_library(mylib UNKNOWN IMPORTED)
set_target_properties(mylib PROPERTIES
IMPORTED_LOCATION "${MYLIB_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${MYLIB_INCLUDE_DIR}"
)
Команди find_path та find_library самі шукають файли в стандартних місцях і в підказках HINTS. Створена IMPORTED ціль поводиться як звичайна: після target_link_libraries(MyApp PRIVATE mylib) заголовки та бібліотека підключаються автоматично.
Спосіб 5: pkg-config (Linux і macOS)
pkg-config зберігає прапорці збірки бібліотек у .pc файлах. Багато бібліотек Linux (GTK, GLib, SDL2) мають такі файли.
cmake_minimum_required(VERSION 3.10)
project(MyProject)
set(CMAKE_CXX_STANDARD 17)
# Шукаємо pkg-config
find_package(PkgConfig REQUIRED)
# Просимо pkg-config знайти бібліотеку
pkg_check_modules(GTK REQUIRED gtk+-3.0)
add_executable(MyApp main.cpp)
# Підключаємо
target_include_directories(MyApp PRIVATE ${GTK_INCLUDE_DIRS})
target_link_libraries(MyApp ${GTK_LIBRARIES})
target_link_directories(MyApp PRIVATE ${GTK_LIBRARY_DIRS})
Сучасніша форма, яка створює імпортовану ціль і позбавляє від трьох окремих команд:
pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
add_executable(MyApp main.cpp)
target_link_libraries(MyApp PRIVATE PkgConfig::GTK)
Недолік способу: pkg-config поширений переважно на Linux, а на Windows його немає за замовчуванням. Для кросплатформного проєкту краще find_package або менеджер пакетів.
Порівняння способів
| Спосіб | Переваги | Недоліки | Коли використовувати |
|---|---|---|---|
find_package | Стандартний підхід, швидкий, бібліотека збирається один раз для всіх проєктів | Бібліотека має бути встановлена заздалегідь | Популярні бібліотеки (OpenCV, Boost, Qt, Threads) |
add_subdirectory | Повний контроль, версія зафіксована в репозиторії | Займає місце в репозиторії, збирається щоразу | Власні модулі, git submodules |
FetchContent | Автоматичне завантаження, версія зафіксована в CMakeLists.txt | Повільна перша збірка, потрібен інтернет | Невеликі бібліотеки, тестові фреймворки |
| Ручне підключення | Працює з будь-чим | Шляхи не переносяться на інші машини | Лише для винятків, коли інші способи недоступні |
pkg-config | Стандарт для системних бібліотек Linux | Немає на Windows | Системні бібліотеки Linux (GTK, SDL2) |
find_package, маленькі залежності через FetchContent, а все, що потребує довгої збірки, через менеджер пакетів. Наприкінці розділу показано, як менеджер пакетів vcpkg перекриває і find_package, і FetchContent.Питання для самоперевірки
Findfmt.cmake, а лише потім Config-файл. Другий одразу вимагає Config-файл fmt-config.cmake, який постачає сама бібліотека. З CONFIG пошук однозначніший, і цей запис використовується з vcpkg.main постійно змінюється. Сьогоднішня збірка і збірка через місяць можуть використовувати різний код бібліотеки з різним API, а помилку буде важко відтворити. Тег або хеш коміту гарантує, що кожен отримає однаковий код.target_link_libraries. Змінні дають лише списки шляхів, які потрібно розкладати по різних командах вручну, і вони не відстежують транзитивні залежності.-DCMAKE_PREFIX_PATH=/шлях або папку з Config-файлом через -DІм'яПакета_DIR=/шлях/lib/cmake/Ім'яПакета. Якщо ви перед цим уже запускали конфігурацію, видаліть build/: невдалий результат зберігається в кеші.Резюме
🔍 find_package
CONFIG надійніший за застарілі модулі Find*.cmake.📂 add_subdirectory
⬇️ FetchContent
🛠️ Вручну і pkg-config
if, опції option, цикли, функції та макроси, а також генерацію заголовка з версією програми через configure_file.Бібліотеки в CMake: STATIC, SHARED, INTERFACE та видимість PUBLIC/PRIVATE
Три типи бібліотек, створення власної бібліотеки, динамічні бібліотеки на Windows, вибір типу через BUILD_SHARED_LIBS, поширення налаштувань між цілями за допомогою PUBLIC, PRIVATE і INTERFACE.
Умови, опції, цикли та функції в CMake
Умовні конструкції if/elseif/else, перевірка змінних і файлів, логічні оператори, платформо-залежні налаштування, опції option, цикли foreach і while, функції та макроси, генерація заголовка з версією через configure_file.