第九讲:查找依赖包——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找包有两种方式,说白了就是「按图索骥」和「直接敲门」的区别。

我画了一张图,帮你理清这两种模式的关系:

find_package() 查找流程 find_package(XXX) 有Config文件? Config模式 找 XXXConfig.cmake 或 xxx-config.cmake Module模式 找 FindXXX.cmake (CMake自带或用户提供) 设置 XXX_FOUND 等变量

说白了,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就是一个导入目标。它把头文件路径、库文件、编译选项都封装好了。你只需要链接它,什么都不用操心。

重点:Config模式查找时,CMake会搜索<PackageName>Config.cmake<lower-case-package-name>-config.cmake。搜索路径包括CMAKE_PREFIX_PATHCMAKE_FIND_ROOT_PATH等变量指定的目录。

9.4 Module模式——你自己写脚本去找

那如果包本身没有提供Config文件呢?比如一些老旧的库,或者你自己写的内部库。这时候就需要Module模式了。

Module模式会去找FindXXX.cmake文件。这个文件可以来自:

  • CMake自带的模块(比如FindThreadsFindZLIB
  • 你自己写的,放在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模块时忘了调用find_package_handle_standard_args。结果XXX_FOUND变量一直没被设置,导致后面的条件判断全部失效。记住,这个函数是必须的,它负责设置XXX_FOUND并处理REQUIRED参数。

9.5 嵌入式项目中的实战经验

做嵌入式开发,跟做桌面软件有个很大的不同——交叉编译。你的开发环境是x86,但目标板是ARM或RISC-V。这时候查找包就变得微妙了。

我建议你注意以下几点:

  1. 区分宿主包和目标包:比如FindPython找的是开发机上的Python,而FindFreeRTOS找的是交叉编译好的FreeRTOS库。CMake通过CMAKE_FIND_ROOT_PATH来区分这两者。
  2. 善用CMAKE_FIND_ROOT_PATH:把交叉编译工具链的sysroot设到这个变量里,CMake就会优先在那里找包。
  3. 写自己的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)
提示:写Find模块时,变量命名要规范。CMake约定:<PackageName>_INCLUDE_DIR<PackageName>_LIBRARY<PackageName>_FOUND。别自己发明新名字,否则别人看不懂。

好了,关于find_package()的内容就讲到这里。Config模式和Module模式各有各的适用场景。我个人建议:如果库本身提供了Config文件,优先用Config模式;如果没有,就自己写一个Find模块。记住,写Find模块时别忘了调用find_package_handle_standard_args——这个坑我替你们踩过了。


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