Cmake

find_package: Module vs Config режимы, написание собственного Find-модуля

Введение

Циклы 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 явно, осознанно обработать этот случай (например, отключить опциональную функциональность, зависящую от этой библиотеки, вместо прерывания всей конфигурации проекта) — выбор между этими двумя вариантами должен явно отражать, действительно ли конкретная зависимость абсолютно необходима для самой возможности собрать проект, или она опциональна.