第十六章:自定义查找模块——编写FindXXX.cmake
说实话,CMake 里最让我头疼的,就是每次引入第三方库时都要手动配置路径。尤其是那些没有提供官方 CMake 支持的库,你得自己写 include_directories、link_directories,还得祈祷路径别写错。
后来我学乖了——自己写一个 FindXXX.cmake 模块,一劳永逸。今天我们就来聊聊,怎么写出一个像模像样的自定义查找模块。
为什么需要 FindXXX.cmake?
你想想看,每次换个环境,库的安装路径可能就变了。Windows 下可能在 C:/Program Files,Linux 下可能在 /usr/local,macOS 下又可能是 Homebrew 的路径。手动改?太累了。
FindXXX.cmake 的作用,就是封装这些查找逻辑。你只需要写一次,以后 find_package(XXX) 就能自动找到头文件和库文件。
find_package(XXX) 返回 XXX_INCLUDE_DIRS 和 XXX_LIBRARIES 两个变量。
编写 FindXXX.cmake 的基本结构
我个人习惯把查找模块分成三个部分:查找头文件、查找库文件、导出变量。咱们一步步来。
第一步:查找头文件
用 find_path 命令。它会去系统默认路径、用户指定的路径里找头文件。
# FindXXX.cmake - 查找 XXX 库
# 查找头文件
find_path(XXX_INCLUDE_DIR
NAMES xxx.h
PATHS
/usr/include
/usr/local/include
/opt/local/include
${CMAKE_INSTALL_PREFIX}/include
PATH_SUFFIXES
xxx
libxxx
)
# 如果没有找到,给出友好提示
if(NOT XXX_INCLUDE_DIR)
message(WARNING "XXX 头文件未找到,请检查 xxx.h 的路径")
endif()
这里有个小技巧:PATH_SUFFIXES 可以指定子目录。比如有些库把头文件放在 /usr/include/xxx/ 下,你加上 xxx 就能直接匹配到。
第二步:查找库文件
用 find_library 命令。它会在系统库路径里找 .a、.so、.dylib 或 .lib 文件。
# 查找库文件
find_library(XXX_LIBRARY
NAMES xxx libxxx
PATHS
/usr/lib
/usr/local/lib
/opt/local/lib
${CMAKE_INSTALL_PREFIX}/lib
PATH_SUFFIXES
xxx
libxxx
)
if(NOT XXX_LIBRARY)
message(WARNING "XXX 库文件未找到,请检查 libxxx 的路径")
endif()
NAMES 参数可以写多个名字。比如 Linux 下叫 libxxx.so,Windows 下叫 xxx.lib,你写 xxx libxxx 就能覆盖两种情况。
第三步:导出变量
找到之后,把结果封装成标准的 XXX_INCLUDE_DIRS 和 XXX_LIBRARIES 变量。
# 设置标准变量
set(XXX_INCLUDE_DIRS ${XXX_INCLUDE_DIR})
set(XXX_LIBRARIES ${XXX_LIBRARY})
# 标记为找到
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(XXX
REQUIRED_VARS XXX_INCLUDE_DIR XXX_LIBRARY
VERSION_VAR XXX_VERSION
)
# 如果找到,添加导入目标
if(XXX_FOUND)
if(NOT TARGET XXX::XXX)
add_library(XXX::XXX UNKNOWN IMPORTED)
set_target_properties(XXX::XXX PROPERTIES
IMPORTED_LOCATION "${XXX_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${XXX_INCLUDE_DIR}"
)
endif()
endif()
target_link_libraries(myapp XXX::XXX),省去手动设置头文件路径的麻烦。
完整示例:FindZLIB.cmake
咱们拿 zlib 举个例子。这是一个很常见的压缩库,很多项目都用它。
# FindZLIB.cmake - 查找 zlib 库
# 查找头文件
find_path(ZLIB_INCLUDE_DIR
NAMES zlib.h
PATHS
/usr/include
/usr/local/include
/opt/homebrew/include # macOS Homebrew
PATH_SUFFIXES
zlib
)
# 查找库文件
find_library(ZLIB_LIBRARY
NAMES z libz
PATHS
/usr/lib
/usr/local/lib
/opt/homebrew/lib
PATH_SUFFIXES
zlib
)
# 处理找到的结果
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(ZLIB
REQUIRED_VARS ZLIB_INCLUDE_DIR ZLIB_LIBRARY
VERSION_VAR ZLIB_VERSION
)
# 设置导出变量
if(ZLIB_FOUND)
set(ZLIB_INCLUDE_DIRS ${ZLIB_INCLUDE_DIR})
set(ZLIB_LIBRARIES ${ZLIB_LIBRARY})
# 创建导入目标
if(NOT TARGET ZLIB::ZLIB)
add_library(ZLIB::ZLIB UNKNOWN IMPORTED)
set_target_properties(ZLIB::ZLIB PROPERTIES
IMPORTED_LOCATION "${ZLIB_LIBRARY}"
INTERFACE_INCLUDE_DIRECTORIES "${ZLIB_INCLUDE_DIR}"
)
endif()
message(STATUS "找到 ZLIB: ${ZLIB_LIBRARY}")
else()
message(WARNING "未找到 ZLIB 库,请安装 zlib-dev")
endif()
使用自定义查找模块
写好了 FindZLIB.cmake,怎么用呢?你需要告诉 CMake 去哪里找这个模块。
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)
# 告诉 CMake 去哪里找自定义模块
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# 使用 find_package
find_package(ZLIB REQUIRED)
# 链接库
add_executable(myapp main.cpp)
target_link_libraries(myapp ZLIB::ZLIB)
把 FindZLIB.cmake 放在项目根目录下的 cmake/ 文件夹里,然后 CMAKE_MODULE_PATH 指向它。搞定。
FindZLIB.cmake 写成了 findzlib.cmake。CMake 对大小写敏感,模块文件名必须和 find_package 的参数完全一致。ZLIB 就对应 FindZLIB.cmake,别搞混了。
查找模块的完整流程图
下面这张图展示了 FindXXX.cmake 的执行流程,从调用 find_package 到最终使用库的完整链路。
避坑指南
写 FindXXX.cmake 时,有几个坑我踩过,分享给你:
- 路径写死:别把路径写死在模块里。用
PATHS参数列出常见路径,或者让用户通过XXX_ROOT环境变量指定。 - 忘记处理版本号:有些库有多个版本,比如
libxxx.so.1.2.3。用find_library的NAMES_PER_DIR选项可以逐个目录查找。 - 忽略跨平台:Windows 下库文件叫
xxx.lib,Linux 下叫libxxx.so。在NAMES里把两种都写上。 - 不提供导入目标:老式的
XXX_INCLUDE_DIRS和XXX_LIBRARIES虽然能用,但不如导入目标方便。建议两者都提供。
总结
写 FindXXX.cmake 其实不难,核心就是 find_path 和 find_library 两个命令。加上导入目标,就能让使用者像用官方库一样方便。
下次你遇到一个没有 CMake 支持的库,不妨花十分钟写个查找模块。相信我,这十分钟的投入,会在未来无数次 cmake .. 中回报你。
公众号:蓝海资料掘金营,微信 deep3321