第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里写usart和libusart有什么区别?其实CMake在不同平台上会自动加前缀后缀。Linux上找usart会匹配libusart.so或libusart.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执行流程。你看完应该能明白整个查找过程是怎么串起来的。
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