9、CMake查找依赖:find_package()机制、Config模式与Module模式、编写FindXXX.cmake模块、pkg-config集成

依赖管理,说白了就是让CMake知道你的代码需要哪些第三方库,以及这些库的头文件和库文件到底在哪。我见过太多新手在find_package()上栽跟头,明明库装好了,CMake就是找不到。嗯,今天我们就彻底把这个机制讲透。

9.1 find_package()的两副面孔:Config模式 vs Module模式

find_package()这个命令,其实有两种工作模式。我个人习惯把它们比作「主动投怀送抱」和「我主动去找你」。

  • Config模式:库的作者已经贴心地生成了XXXConfig.cmake文件,CMake直接读取这个文件就能拿到所有信息。这是现代CMake推荐的方式。
  • Module模式:库的作者没提供Config文件,但CMake自带或你写了一个FindXXX.cmake模块,由这个模块去探测库的位置。

CMake会先尝试Config模式,如果找不到对应的Config文件,再回退到Module模式。当然,你也可以用MODULE关键字强制走Module模式。

核心区别一句话总结:Config模式是库主动告诉你「我在这」,Module模式是你自己去找「你在哪」。

9.2 Config模式:库作者的责任

Config模式之所以现代,是因为它把查找逻辑交给了库的作者。库在安装时,会生成一个XXXConfig.cmake文件,里面定义了库的目标(targets)、包含路径、链接库等。

举个例子,你安装OpenCV后,通常会在/usr/local/lib/cmake/opencv4/下找到OpenCVConfig.cmake。然后你在CMakeLists.txt里写:

find_package(OpenCV REQUIRED)
target_link_libraries(myapp PRIVATE ${OpenCV_LIBS})

CMake会去/usr/local/lib/cmake//usr/lib/cmake/等标准路径下找OpenCVConfig.cmake。你也可以通过设置CMAKE_PREFIX_PATH变量来告诉CMake额外的搜索路径。

我的经验:我在项目中遇到过好几次Config文件版本不匹配的问题。比如系统装了OpenCV 3,但项目需要OpenCV 4。这时候可以用find_package(OpenCV 4.0 EXACT REQUIRED)来精确控制版本。

9.3 Module模式:自己动手丰衣足食

有些老旧的库,或者那些不按CMake规范来的库,不会生成Config文件。这时候就需要Module模式出马了。

CMake自带了很多FindXXX.cmake模块,比如FindBoost.cmakeFindZLIB.cmake。这些模块通常放在CMake安装目录的Modules文件夹下。你只需要写:

find_package(Boost REQUIRED COMPONENTS filesystem system)

CMake就会自动调用FindBoost.cmake去查找Boost库。

但如果你用的库比较冷门,CMake没带对应的Find模块,那就得自己写了。嗯,这里有个坑——我曾经花了一整天调试一个FindXXX.cmake,最后发现是路径写错了。

9.4 编写FindXXX.cmake模块

写一个Find模块其实不复杂,核心就是找到头文件路径和库文件路径,然后设置好变量。一个典型的Find模块长这样:

# FindFoo.cmake
# 查找Foo库

find_path(FOO_INCLUDE_DIR foo.h
  PATHS /usr/include /usr/local/include /opt/foo/include
)

find_library(FOO_LIBRARY foo
  PATHS /usr/lib /usr/local/lib /opt/foo/lib
)

if(FOO_INCLUDE_DIR AND FOO_LIBRARY)
  set(FOO_FOUND TRUE)
  set(FOO_INCLUDE_DIRS ${FOO_INCLUDE_DIR})
  set(FOO_LIBRARIES ${FOO_LIBRARY})
  message(STATUS "Found Foo: ${FOO_LIBRARY}")
else()
  set(FOO_FOUND FALSE)
  message(WARNING "Foo not found")
endif()

mark_as_advanced(FOO_INCLUDE_DIR FOO_LIBRARY)

然后你在项目里就可以用find_package(Foo REQUIRED)了。注意,Find模块的文件名必须和包名一致,且放在cmake/Modules/目录下,并通过set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules")告诉CMake去哪找。

避坑指南:我曾经写过一个Find模块,忘了处理REQUIREDQUIET参数。结果用户用了find_package(Foo REQUIRED),库没找到时CMake居然没报错,而是继续编译,最后链接失败。记住,一定要检查Foo_FOUND并调用find_package_handle_standard_args()

9.5 pkg-config集成:老牌工具的新生

pkg-config是Linux世界的老牌依赖管理工具。很多库会提供.pc文件,里面包含了编译和链接参数。CMake可以通过FindPkgConfig.cmake模块来集成pkg-config。

用法很简单:

find_package(PkgConfig REQUIRED)
pkg_check_modules(FOO REQUIRED foo-1.0)

target_include_directories(myapp PRIVATE ${FOO_INCLUDE_DIRS})
target_link_libraries(myapp PRIVATE ${FOO_LIBRARIES})

pkg-config的好处是跨平台一致性。你在Ubuntu上装库,.pc文件会自动安装到/usr/lib/pkgconfig/。macOS上用Homebrew也一样。我个人习惯在Linux上优先用pkg-config,因为很多系统库都支持。

小技巧:如果你不确定某个库是否支持pkg-config,可以试试在终端执行pkg-config --cflags --libs xxx。如果返回了正确的编译参数,那恭喜你,可以直接用pkg_check_modules

9.6 知识体系总览

下面这张图总结了CMake依赖查找的完整流程。你仔细看,其实核心就两条路径:Config模式和Module模式。pkg-config只是Module模式下的一个特殊实现。

CMake依赖查找流程 find_package() 有Config文件? Config模式 Module模式 读取XXXConfig.cmake 定义IMPORTED目标 执行FindXXX.cmake 设置XXX_FOUND等变量 使用目标/变量 Config模式 Module模式

9.7 实战建议

说了这么多,到底该用哪种方式?我的建议是:

  1. 优先用Config模式。如果库提供了Config文件,直接用,省心省力。
  2. 次选pkg-config。如果库不支持Config模式但提供了.pc文件,用pkg_check_modules
  3. 最后自己写Find模块。只有前两种都不行时,才考虑手写FindXXX.cmake

你想想看,写一个Find模块其实不难,但维护起来很麻烦。库升级了、路径变了,你的Find模块就得跟着改。而Config模式是库作者维护的,你只管用就行。

一句话总结find_package()是CMake依赖管理的核心,理解Config模式和Module模式的区别,能让你少踩很多坑。pkg-config是Module模式下的一个强大工具,值得掌握。


公众号:蓝海资料掘金营,微信deep3321