Введение
Циклы Qt Multimedia, OpenCV (статья 561) и PySide6 неоднократно использовали find_package() для подключения внешних библиотек без подробного объяснения внутренней механики — в этой статье разберём два различающихся режима работы этой команды (Module и Config) и напишем собственный простой Find-модуль для библиотеки, не предоставляющей готовую поддержку CMake.
Концепция
Module-режим find_package() использует файл Find<Имя>.cmake (либо встроенный в саму установку CMake для широко распространённых библиотек, либо предоставленный самим проектом в специальной директории) — этот файл содержит явную, написанную вручную логику поиска заголовочных файлов и библиотечных файлов в типичных для данной библиотеки местах установки в системе. Config-режим (более современный, предпочтительный там, где он доступен) использует файл <Имя>Config.cmake, который сама библиотека (если она современна и правильно поддерживает CMake) устанавливает вместе с собой при инсталляции (статья 582 — про экспорт и установку пакетов) — этот файл уже точно знает все нужные пути и автоматически создаёт готовые, правильно настроенные импортированные цели (imported targets), что значительно надёжнее и удобнее, чем «угадывающая» логика Module-режима, написанная заранее, без знания реальной, конкретной установки библиотеки.
Пример кода: find_package
# Config-режим — современный, предпочтительный путь для библиотек с готовой поддержкой CMake
find_package(OpenCV REQUIRED CONFIG) # явное указание CONFIG — хотя обычно срабатывает и без него
target_link_libraries(MyApp PRIVATE ${OpenCV_LIBS}) # либо, в более современном стиле:
# target_link_libraries(MyApp PRIVATE OpenCV::opencv_core OpenCV::opencv_imgproc) # импортированные цели
# FindMyCustomLib.cmake — написание собственного Find-модуля для библиотеки без готовой поддержки CMake
# (размещается в специальной директории, добавленной в CMAKE_MODULE_PATH)
find_path(MYCUSTOMLIB_INCLUDE_DIR mycustomlib.h
PATHS /usr/local/include /opt/mycustomlib/include
)
find_library(MYCUSTOMLIB_LIBRARY
NAMES mycustomlib
PATHS /usr/local/lib /opt/mycustomlib/lib
)
include(FindPackageHandleStandardArgs) # стандартный, переиспользуемый механизм проверки результата
find_package_handle_standard_args(MyCustomLib
REQUIRED_VARS MYCUSTOMLIB_LIBRARY MYCUSTOMLIB_INCLUDE_DIR
)
if(MyCustomLib_FOUND AND NOT TARGET MyCustomLib::MyCustomLib)
add_library(MyCustomLib::MyCustomLib UNKNOWN IMPORTED) # создание импортированной цели, как у Config-режима
set_target_properties(MyCustomLib::MyCustomLib PROPERTIES
IMPORTED_LOCATION "${MYCUSTOMLIB_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${MYCUSTOMLIB_INCLUDE_DIR}"
)
endif()
# Использование собственного Find-модуля — синтаксически идентично использованию встроенных модулей
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
find_package(MyCustomLib REQUIRED)
target_link_libraries(MyApp PRIVATE MyCustomLib::MyCustomLib)
Пояснения к коду
Сравнение ${OpenCV_LIBS} (старый, переменно-ориентированный стиль) и OpenCV::opencv_core (современный стиль импортированных целей) показывает эволюцию практики использования результата find_package — современный стиль с явными, именованными целями даёт более точный контроль над видимостью зависимостей (статья 575) для каждого конкретного модуля библиотеки, а не одну общую переменную со всеми библиотеками сразу. Собственный FindMyCustomLib.cmake показывает классическую структуру Module-режима — find_path/find_library выполняют реальный поиск в типичных местах установки (с возможностью дополнительных подсказок через переменные среды или явно переданные пользователем пути), а find_package_handle_standard_args — стандартный, многократно используемый механизм, формирующий согласованный, ожидаемый набор переменных результата (<Имя>_FOUND) и выводящий понятные сообщения об успехе/неудаче поиска, согласованные по формату с поведением встроенных Find-модулей CMake. Создание IMPORTED-цели внутри собственного Find-модуля показывает современную практику — даже написанный «вручную» Module-режим может (и должен, согласно современным рекомендациям) предоставлять результат в виде готовой, настроенной цели, а не «сырых» переменных путей, давая пользователям этого Find-модуля тот же, единообразный, современный опыт использования через target_link_libraries, что и для библиотек с готовой, нативной поддержкой Config-режима.
Подводные камни
- Предположение, что
find_package()без явного указанияCONFIG/MODULEвсегда находит то, что ожидает разработчик, особенно если в системе одновременно присутствуют и встроенныйFind<Имя>.cmakeмодуль, и установленный<Имя>Config.cmakeот самой библиотеки — без явного указания режима CMake пытается оба варианта в определённом, документированном порядке, и для случаев, где это разрешение неоднозначности критично, явное указание желаемого режима (CONFIGилиMODULE) даёт более предсказуемое, не зависящее от точной конфигурации конкретной системы поведение. - Написание собственного Find-модуля с захардкоженными, специфичными для одной конкретной платформы путями поиска, без учёта того, что то же самое описание сборки потенциально должно работать на разных платформах (Linux, Windows, macOS) с существенно различающимися типичными местами установки библиотек — качественный, переносимый Find-модуль должен учитывать эти платформенные различия, обычно через условную логику (
if(WIN32)/if(APPLE)) или, что предпочтительнее, через использование стандартных, кроссплатформенных механизмов поиска CMake (переменные средыPATH, стандартные системные директории для конкретной платформы), а не жёстко прописанные, специфичные для одной платформы абсолютные пути. - Отсутствие явной проверки
NOT TARGET MyCustomLib::MyCustomLibперед созданием импортированной цели — посколькуfind_package()для одной и той же библиотеки потенциально может быть вызван несколько раз в пределах одного, особенно крупного, многомодульного проекта (статья 584 итоговый проект цикла), повторная попытка создать цель с тем же именем (без проверки, что она уже существует от предыдущего вызова) приведёт к ошибке конфигурации о попытке повторного определения уже существующей цели. - Игнорирование разницы между
REQUIREDи опциональнымfind_package()—find_package(MyLib REQUIRED)немедленно прерывает конфигурацию с понятной ошибкой, если библиотека не найдена, тогда какfind_package(MyLib)безREQUIREDлишь устанавливает переменнуюMyLib_FOUNDвFALSE, позволяя кодуCMakeLists.txtявно, осознанно обработать этот случай (например, отключить опциональную функциональность, зависящую от этой библиотеки, вместо прерывания всей конфигурации проекта) — выбор между этими двумя вариантами должен явно отражать, действительно ли конкретная зависимость абсолютно необходима для самой возможности собрать проект, или она опциональна.