11、CMake中的查找依赖:find_package()详解、Find模块与Config模块、find_library()与find_file()
依赖管理,说白了就是让CMake帮你找到别人写好的代码。我刚开始用CMake那会儿,最头疼的就是这步。明明库装好了,CMake就是找不到,气得我差点想手写Makefile。后来摸透了里面的门道,才发现其实就那么回事。
今天咱们就把CMake查找依赖的几把刷子,一个一个捋清楚。
11.1 find_package():查找依赖的入口
find_package()是CMake里最常用的查找命令。它的工作模式有两种:模块模式和配置模式。
基本语法长这样:
find_package(<PackageName> [version] [EXACT] [QUIET] [MODULE]
[REQUIRED] [[COMPONENTS] [components...]]
[OPTIONAL_COMPONENTS components...]
[NO_POLICY_SCOPE])
几个关键参数我解释一下:
- REQUIRED:找不到就报错,直接终止构建。我一般都会加上,免得后面编译时出现一堆莫名其妙的错误。
- QUIET:找不到也别嚷嚷,安静点。调试时建议先不加,看看CMake到底在找什么。
- COMPONENTS:指定需要哪些组件。比如找Qt,你可能只需要QtWidgets和QtCore。
- EXACT:版本必须精确匹配,不能向下兼容。
举个例子:
find_package(OpenCV REQUIRED COMPONENTS core imgproc highgui)
if(OpenCV_FOUND)
include_directories(${OpenCV_INCLUDE_DIRS})
target_link_libraries(myapp ${OpenCV_LIBS})
endif()
这里有个坑——大小写敏感。OpenCV写成opencv,有时候能行,有时候不行。我个人习惯严格按照官方文档来,包名首字母大写。
11.2 模块模式 vs 配置模式
这两种模式的区别,我画了张图,一看就明白:
模块模式,就是找Find<Package>.cmake文件。这些文件通常由CMake官方提供,或者第三方库自己写的。比如FindBoost.cmake、FindPython3.cmake。
配置模式,是找库自己安装时生成的<Package>Config.cmake或<lower-case-package>-config.cmake。比如OpenCV安装后会生成OpenCVConfig.cmake。
CMake默认先试模块模式,找不到再试配置模式。你可以用MODULE关键字强制只用模块模式。
11.3 Find模块 vs Config模块
咱们来对比一下这两种模块:
| 对比项 | Find模块 | Config模块 |
|---|---|---|
| 文件名 | Find<Package>.cmake | <Package>Config.cmake |
| 提供者 | CMake官方或第三方 | 库的开发者 |
| 搜索路径 | CMAKE_MODULE_PATH | CMAKE_PREFIX_PATH |
| 可靠性 | 一般,可能版本滞后 | 高,与库版本同步 |
| 自定义程度 | 高,可以自己写 | 低,由库决定 |
我曾经接手过一个老项目,里面用的还是Find模块找OpenCV。结果OpenCV升级到4.x后,FindOpenCV.cmake里的一些变量名变了,编译直接挂掉。后来改成配置模式,问题就解决了。
如果你自己写了一个库,想让别人方便地用find_package()找到,那就得生成Config文件。CMake提供了install(EXPORT)和configure_package_config_file()来帮忙。
# 生成导出目标
install(TARGETS mylib EXPORT MyLibTargets
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
INCLUDES DESTINATION include
)
# 安装导出文件
install(EXPORT MyLibTargets
FILE MyLibTargets.cmake
NAMESPACE MyLib::
DESTINATION lib/cmake/MyLib
)
# 生成Config文件
include(CMakePackageConfigHelpers)
configure_package_config_file(
${CMAKE_CURRENT_SOURCE_DIR}/MyLibConfig.cmake.in
${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
INSTALL_DESTINATION lib/cmake/MyLib
PATH_VARS INCLUDE_INSTALL_DIR
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/MyLibConfig.cmake
DESTINATION lib/cmake/MyLib
)
11.4 find_library():精确查找库文件
有时候find_package()搞不定,或者你只想找一个单独的库文件,那就用find_library()。
find_library(<VAR> name1 [path1 path2 ...])
它会搜索系统路径和用户指定的路径,找到后把完整路径存到变量里。
find_library(MY_LIB NAMES mylib mylib_d
PATHS /usr/local/lib /opt/lib
PATH_SUFFIXES lib64 lib
)
if(MY_LIB)
message(STATUS "Found mylib: ${MY_LIB}")
target_link_libraries(myapp ${MY_LIB})
else()
message(FATAL_ERROR "mylib not found!")
endif()
几个要点:
- NAMES:可以指定多个名字,CMake会依次尝试。比如debug版和release版名字不同。
- PATHS:指定搜索路径。我一般把常用的第三方库路径都列上。
- PATH_SUFFIXES:在搜索路径下再追加子目录。比如有些库把64位版本放在lib64下。
- NO_DEFAULT_PATH:不要搜索系统默认路径。慎用,除非你知道自己在干什么。
CMAKE_SIZEOF_VOID_P。
11.5 find_file():查找任意文件
find_file()和find_library()用法几乎一样,只不过它找的是任意文件,不限于库文件。
find_file(<VAR> name1 [path1 path2 ...])
我常用它来找配置文件、头文件、或者资源文件。
find_file(CONFIG_FILE config.ini
PATHS ${CMAKE_SOURCE_DIR}/config
/etc/myapp
$ENV{HOME}/.myapp
)
if(CONFIG_FILE)
message(STATUS "Config file: ${CONFIG_FILE}")
# 把配置文件安装到目标位置
install(FILES ${CONFIG_FILE} DESTINATION etc)
endif()
这里有个小技巧:用$ENV{}可以读取环境变量。比如$ENV{HOME}就是用户主目录。
11.6 实战:组合使用
实际项目中,我经常把find_package()和find_library()组合使用。比如一个库既有Config文件,又有一些额外的依赖库:
# 先尝试用find_package找主库
find_package(MyLib QUIET)
if(MyLib_FOUND)
message(STATUS "Found MyLib via config mode")
target_link_libraries(myapp MyLib::MyLib)
else()
# 找不到就手动找
find_library(MYLIB_LIB mylib
PATHS /usr/local/lib /opt/mylib/lib
)
find_path(MYLIB_INCLUDE mylib.h
PATHS /usr/local/include /opt/mylib/include
)
if(MYLIB_LIB AND MYLIB_INCLUDE)
message(STATUS "Found MyLib manually")
target_include_directories(myapp PRIVATE ${MYLIB_INCLUDE})
target_link_libraries(myapp ${MYLIB_LIB})
else()
message(FATAL_ERROR "MyLib not found!")
endif()
endif()
这样写的好处是:优先用配置模式,干净利落;不行再手动找,兜底方案。嗯,我在好几个跨平台项目里都是这么干的,效果不错。
11.7 调试技巧
找依赖出了问题,别慌。我一般按这个顺序排查:
- 加--debug-find参数:
cmake --debug-find ..,CMake会打印详细的查找过程。 - 检查CMAKE_PREFIX_PATH:看看这个变量有没有设置对。很多新手都栽在这。
- 手动搜索:用
find / -name "*Config.cmake" 2>/dev/null看看文件到底在哪。 - 检查版本:有时候找到了,但版本不匹配。用
find_package的EXACT参数测试一下。
message(STATUS "CMAKE_PREFIX_PATH: ${CMAKE_PREFIX_PATH}"),一眼就能看到搜索路径对不对。
好了,关于CMake查找依赖的这些东西,咱们就聊到这儿。记住一句话:先找Config,再找Find,最后手动找。这个顺序能解决90%的问题。
剩下的10%,嗯,那就得具体情况具体分析了。不过有了今天这些知识,你应该能应付大部分场景了。
公众号:蓝海资料掘金营,微信 deep3321