第九讲:查找依赖包——find_package()、FindXXX.cmake模块、Config模式与Module模式
各位同学,今天我们来聊聊嵌入式项目中一个绕不开的话题——怎么找到并引入别人写好的库。
你想想看,我们做嵌入式开发,很少是从零开始写所有代码的。总要依赖一些现成的库:FreeRTOS、CMSIS、LwIP、FatFS……这些库怎么被CMake找到并正确链接到你的项目里?这就是find_package()要干的事。
我个人习惯把find_package()比作「找东西的管家」。你告诉它要找什么,它就去系统里翻箱倒柜地找。但怎么找、找什么格式的包,这里面门道不少。
9.1 find_package() 的基本用法
先看一个最简单的例子:
find_package(CMSIS REQUIRED)
target_link_libraries(my_firmware CMSIS::CMSIS)
这里REQUIRED关键字的意思是——找不到就报错,别藏着掖着。我刚开始用CMake时,有次忘了加REQUIRED,结果编译到一半才报链接错误,排查了半天……嗯,从那以后我再也不敢省这个关键字了。
除了REQUIRED,还有几个常用参数:
| 参数 | 含义 | 我的建议 |
|---|---|---|
| REQUIRED | 必须找到,否则报错 | 核心依赖一定要加 |
| QUIET | 找不到也不报错,静默跳过 | 可选依赖可以用 |
| COMPONENTS | 指定需要包中的哪些组件 | 按需引入,别贪多 |
| CONFIG / MODULE | 强制指定查找模式 | 后面会细讲 |
find_package(XXX QUIET)试一下,再用if(XXX_FOUND)做条件判断。这样既不会中断构建,又能灵活处理。
9.2 两种查找模式:Module 模式 vs Config 模式
CMake找包有两种方式,说白了就是「按图索骥」和「直接敲门」的区别。
我画了一张图,帮你理清这两种模式的关系:
说白了,CMake会先尝试Config模式——看看包自己有没有提供XXXConfig.cmake文件。如果找不到,就退而求其次,用Module模式去找FindXXX.cmake脚本。
9.3 Config模式——包自己告诉你它在哪
Config模式是CMake最推荐的方式。为什么?因为包自己最清楚自己的文件结构。
比如你安装了某个SDK,它会在安装目录下放一个SDKConfig.cmake文件。这个文件里定义了:
- 头文件路径(
XXX_INCLUDE_DIRS) - 库文件路径(
XXX_LIBRARIES) - 编译选项(
XXX_COMPILE_DEFINITIONS) - 甚至还有导入目标(Imported Targets)
我个人特别喜欢导入目标这个特性。你看:
# Config模式找到后,通常可以直接用
find_package(CMSIS CONFIG REQUIRED)
target_link_libraries(my_app CMSIS::Core)
这里的CMSIS::Core就是一个导入目标。它把头文件路径、库文件、编译选项都封装好了。你只需要链接它,什么都不用操心。
<PackageName>Config.cmake或<lower-case-package-name>-config.cmake。搜索路径包括CMAKE_PREFIX_PATH、CMAKE_FIND_ROOT_PATH等变量指定的目录。
9.4 Module模式——你自己写脚本去找
那如果包本身没有提供Config文件呢?比如一些老旧的库,或者你自己写的内部库。这时候就需要Module模式了。
Module模式会去找FindXXX.cmake文件。这个文件可以来自:
- CMake自带的模块(比如
FindThreads、FindZLIB) - 你自己写的,放在
CMAKE_MODULE_PATH指定的目录下
我曾经在一个项目里需要找一个很冷门的传感器驱动库,网上根本没有现成的Find模块。没办法,只能自己写一个。嗯,这里分享一下我当时写的模板:
# FindMySensor.cmake
# 我自己写的查找模块
# 先找头文件
find_path(MySensor_INCLUDE_DIR
NAMES my_sensor.h
PATHS /usr/local/include /opt/mysensor/include
)
# 再找库文件
find_library(MySensor_LIBRARY
NAMES mysensor libmysensor
PATHS /usr/local/lib /opt/mysensor/lib
)
# 处理找到/没找到的情况
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MySensor
REQUIRED_VARS MySensor_INCLUDE_DIR MySensor_LIBRARY
)
# 如果找到了,创建导入目标
if(MySensor_FOUND AND NOT TARGET MySensor::MySensor)
add_library(MySensor::MySensor UNKNOWN IMPORTED)
set_target_properties(MySensor::MySensor PROPERTIES
IMPORTED_LOCATION "${MySensor_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${MySensor_INCLUDE_DIR}"
)
endif()
find_package_handle_standard_args。结果XXX_FOUND变量一直没被设置,导致后面的条件判断全部失效。记住,这个函数是必须的,它负责设置XXX_FOUND并处理REQUIRED参数。
9.5 嵌入式项目中的实战经验
做嵌入式开发,跟做桌面软件有个很大的不同——交叉编译。你的开发环境是x86,但目标板是ARM或RISC-V。这时候查找包就变得微妙了。
我建议你注意以下几点:
- 区分宿主包和目标包:比如
FindPython找的是开发机上的Python,而FindFreeRTOS找的是交叉编译好的FreeRTOS库。CMake通过CMAKE_FIND_ROOT_PATH来区分这两者。 - 善用
CMAKE_FIND_ROOT_PATH:把交叉编译工具链的sysroot设到这个变量里,CMake就会优先在那里找包。 - 写自己的Find模块时,要考虑交叉编译:搜索路径不要写死,尽量用
CMAKE_FIND_ROOT_PATH作为基准。
举个例子,我在一个STM32项目里是这样配置的:
# 设置交叉编译根路径
set(CMAKE_FIND_ROOT_PATH /opt/gcc-arm-none-eabi/arm-none-eabi)
# 查找CMSIS包
find_package(CMSIS REQUIRED)
# 查找FreeRTOS
find_package(FreeRTOS REQUIRED COMPONENTS Heap4)
这里CMSIS和FreeRTOS的Config文件都安装在/opt/gcc-arm-none-eabi/arm-none-eabi下,CMake通过CMAKE_FIND_ROOT_PATH就能找到它们。
9.6 实战:自己写一个Find模块
最后,我带大家手写一个完整的Find模块。假设我们有一个叫UltraLib的库,它包含一个头文件和一个静态库。
第一步,在项目里创建cmake/FindUltraLib.cmake:
# cmake/FindUltraLib.cmake
# 我自己写的查找模块
# 查找头文件
find_path(UltraLib_INCLUDE_DIR
NAMES ultralib.h
PATHS ${CMAKE_FIND_ROOT_PATH}/include
/usr/local/include
/opt/ultralib/include
)
# 查找库文件
find_library(UltraLib_LIBRARY
NAMES ultralib
PATHS ${CMAKE_FIND_ROOT_PATH}/lib
/usr/local/lib
/opt/ultralib/lib
)
# 标准处理
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(UltraLib
REQUIRED_VARS UltraLib_INCLUDE_DIR UltraLib_LIBRARY
)
# 创建导入目标
if(UltraLib_FOUND AND NOT TARGET UltraLib::UltraLib)
add_library(UltraLib::UltraLib STATIC IMPORTED)
set_target_properties(UltraLib::UltraLib PROPERTIES
IMPORTED_LOCATION "${UltraLib_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${UltraLib_INCLUDE_DIR}"
)
endif()
# 输出调试信息
mark_as_advanced(UltraLib_INCLUDE_DIR UltraLib_LIBRARY)
第二步,在CMakeLists.txt里使用它:
# 告诉CMake去哪找我们的Find模块
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
# 查找UltraLib
find_package(UltraLib REQUIRED)
# 链接到目标
target_link_libraries(my_firmware PRIVATE UltraLib::UltraLib)
<PackageName>_INCLUDE_DIR、<PackageName>_LIBRARY、<PackageName>_FOUND。别自己发明新名字,否则别人看不懂。
好了,关于find_package()的内容就讲到这里。Config模式和Module模式各有各的适用场景。我个人建议:如果库本身提供了Config文件,优先用Config模式;如果没有,就自己写一个Find模块。记住,写Find模块时别忘了调用find_package_handle_standard_args——这个坑我替你们踩过了。