第10章:自定义查找模块——编写FindXXX.cmake

说实话,刚接触CMake那会儿,我对find_package()这个命令是又爱又恨。爱的是它确实方便,一行代码就能找到第三方库;恨的是,一旦找不到库,报错信息简直让人抓狂。后来我才明白,真正的问题往往出在缺少一个合适的FindXXX.cmake模块

这一章,我们就来聊聊怎么写自己的查找模块。我会把find_path()find_library()find_file()这几个核心命令讲透。嗯,这些都是我踩过坑之后才真正理解的。

10.1 为什么需要自定义FindXXX.cmake?

嵌入式项目里,第三方库的安装位置往往很随意。有的放在/opt下,有的藏在用户目录里,还有的直接跟项目源码混在一起。CMake自带的查找模块,说白了只能覆盖常见情况。

我遇到过最典型的一个场景:某款MCU的SDK,头文件放在sdk/include,库文件却放在sdk/lib/m4f(针对不同内核还有不同子目录)。标准find_package()根本搞不定这种奇葩结构。怎么办?自己写一个FindXXX.cmake呗。

核心思路:FindXXX.cmake的本质,就是告诉CMake「去哪里找头文件、去哪里找库文件、找到后暴露哪些变量」。说白了,就是个路径引导脚本。

10.2 三个核心命令:find_path、find_library、find_file

这三个命令是自定义查找模块的基石。咱们一个一个说。

10.2.1 find_path()——找头文件路径

这个命令用来定位头文件所在的目录。比如你要找usart.h,它返回的是包含这个文件的目录路径,而不是文件本身。

find_path(USART_INCLUDE_DIR
    NAMES usart.h
    PATHS /usr/local/include
          /opt/sdk/include
          ${CMAKE_SOURCE_DIR}/third_party/sdk/include
    PATH_SUFFIXES serial
)

这里有个细节:PATH_SUFFIXES是追加的子目录。比如你指定了/opt/sdk/include,CMake会额外搜索/opt/sdk/include/serial。我个人习惯把常见的后缀都列上,省得漏掉。

小技巧NAMES可以写多个文件名。CMake会按顺序找,找到第一个就停。我一般把最常用的文件名放前面。

10.2.2 find_library()——找库文件

这个命令用来定位.a.so.lib文件。注意,它返回的是库文件的完整路径(包括文件名)。

find_library(USART_LIBRARY
    NAMES usart libusart
    PATHS /usr/local/lib
          /opt/sdk/lib
          ${CMAKE_SOURCE_DIR}/third_party/sdk/lib
    PATH_SUFFIXES m4f
                  armv7
)

你可能会问:NAMES里写usartlibusart有什么区别?其实CMake在不同平台上会自动加前缀后缀。Linux上找usart会匹配libusart.solibusart.a;Windows上找usart会匹配usart.lib。写全了更保险。

注意:嵌入式项目经常有静态库和动态库混用的情况。我建议在find_library()之前先用set(CMAKE_FIND_LIBRARY_SUFFIXES .a)限定一下,避免找到动态库导致链接失败。

10.2.3 find_file()——找任意文件

这个命令比较灵活,可以找任何类型的文件。我常用它来定位链接脚本、配置文件或者二进制工具。

find_file(USART_LINKER_SCRIPT
    NAMES usart.ld
    PATHS ${CMAKE_SOURCE_DIR}/linker
          ${CMAKE_SOURCE_DIR}/config
)

嗯,这里要注意:find_file()返回的是文件完整路径,不是目录。如果你需要目录,可以用get_filename_component()提取。

10.3 完整的FindXXX.cmake示例

光讲理论没意思,咱们直接看一个完整的例子。这是我之前给某款国产MCU写的查找模块,稍微简化了一下。

# FindMySDK.cmake
# 用法:find_package(MySDK REQUIRED)

# 查找头文件
find_path(MySDK_INCLUDE_DIR
    NAMES my_sdk.h
    PATHS /opt/my_sdk/include
          ${CMAKE_SOURCE_DIR}/third_party/my_sdk/include
    PATH_SUFFIXES core
                  hal
)

# 查找库文件
find_library(MySDK_LIBRARY
    NAMES mysdk libmysdk
    PATHS /opt/my_sdk/lib
          ${CMAKE_SOURCE_DIR}/third_party/my_sdk/lib
    PATH_SUFFIXES ${CMAKE_SYSTEM_PROCESSOR}
)

# 查找工具链文件
find_file(MySDK_TOOLCHAIN
    NAMES my_toolchain.cmake
    PATHS /opt/my_sdk/cmake
          ${CMAKE_SOURCE_DIR}/cmake
)

# 处理找到/没找到的情况
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(MySDK
    REQUIRED_VARS MySDK_INCLUDE_DIR MySDK_LIBRARY
    VERSION_VAR MySDK_VERSION
)

# 设置导入目标
if(MySDK_FOUND AND NOT TARGET MySDK::MySDK)
    add_library(MySDK::MySDK UNKNOWN IMPORTED)
    set_target_properties(MySDK::MySDK PROPERTIES
        IMPORTED_LOCATION "${MySDK_LIBRARY}"
        INTERFACE_INCLUDE_DIRECTORIES "${MySDK_INCLUDE_DIR}"
    )
endif()

# 缓存变量
mark_as_advanced(MySDK_INCLUDE_DIR MySDK_LIBRARY MySDK_TOOLCHAIN)

这段代码里,find_package_handle_standard_args是个好东西。它会自动检查必须的变量是否找到,没找到就报错。省得你手动写一堆if(NOT ...)

10.4 知识体系:查找模块的核心逻辑

下面这张图,是我梳理的FindXXX.cmake执行流程。你看完应该能明白整个查找过程是怎么串起来的。

FindXXX.cmake 执行流程 find_package(XXX) find_path() → 定位头文件目录 find_library() → 定位库文件路径 find_file() → 定位其他依赖文件 find_package_handle_standard_args() 设置导入目标 / 报错

10.5 避坑指南:我踩过的几个坑

写FindXXX.cmake看着简单,实际用起来全是细节。我把自己踩过的坑列出来,你遇到了能少走弯路。

  • 路径缓存问题:CMake会缓存查找结果。如果你改了库的位置,记得删掉CMakeCache.txt重新跑。我曾经因为这个折腾了一下午,最后发现是缓存没清。
  • 变量命名冲突:不同模块的变量名不要用太通用的,比如INCLUDE_DIR。我习惯加模块名前缀,比如MySDK_INCLUDE_DIR,这样不会跟其他模块打架。
  • 跨平台路径分隔符:Windows用反斜杠,Linux用正斜杠。CMake内部会自动处理,但如果你手动拼接路径,记得用/或者cmake -E命令。
  • 找不到库时的提示信息:默认的错误提示很简陋。我建议在find_package_handle_standard_args里加上FAIL_MESSAGE参数,告诉用户去哪下载SDK。

特别提醒:嵌入式项目的库文件经常有不同版本(比如针对不同MCU内核)。我建议在FindXXX.cmake里加一个VERSION参数检查,避免链接了错误的库。别问我怎么知道的——有一次我把M4的库链接到了M7的项目上,调试了三天才发现。

10.6 实战建议:如何组织你的查找模块

项目大了之后,查找模块会越来越多。我一般这么组织:

目录 用途 示例
cmake/modules/ 项目自带的查找模块 FindMySDK.cmake
cmake/Modules/ 覆盖CMake内置模块 FindThreads.cmake
third_party/*/cmake/ 第三方库自带的模块 FindLibUSB.cmake

然后在主CMakeLists.txt里设置:

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/Modules")

这样find_package()就会优先搜索你自定义的模块。嗯,这个顺序很重要——同名模块会覆盖,所以把项目自带的放前面。

个人习惯:我会在FindXXX.cmake文件头部加一段注释,写明这个模块的版本、作者、以及依赖关系。半年后回头看,你会感谢自己的。

好了,关于自定义查找模块的内容就这些。记住,写FindXXX.cmake的核心就三件事:找头文件、找库文件、处理找不到的情况。把这三点做好,你的项目就能优雅地管理各种第三方依赖了。


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