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

说实话,CMake 里最让我头疼的,就是每次引入第三方库时都要手动配置路径。尤其是那些没有提供官方 CMake 支持的库,你得自己写 include_directorieslink_directories,还得祈祷路径别写错。

后来我学乖了——自己写一个 FindXXX.cmake 模块,一劳永逸。今天我们就来聊聊,怎么写出一个像模像样的自定义查找模块。

为什么需要 FindXXX.cmake?

你想想看,每次换个环境,库的安装路径可能就变了。Windows 下可能在 C:/Program Files,Linux 下可能在 /usr/local,macOS 下又可能是 Homebrew 的路径。手动改?太累了。

FindXXX.cmake 的作用,就是封装这些查找逻辑。你只需要写一次,以后 find_package(XXX) 就能自动找到头文件和库文件。

核心目标:find_package(XXX) 返回 XXX_INCLUDE_DIRSXXX_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_DIRSXXX_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()
我的经验: 加上导入目标(Imported Target)是现代 CMake 的推荐做法。这样使用者可以直接 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 执行流程 find_package(XXX) 查找 FindXXX.cmake find_path(XXX_INCLUDE_DIR) 查找头文件路径 find_library(XXX_LIBRARY) 查找库文件路径 导出变量 + 导入目标 target_link_libraries 注:find_path 和 find_library 是并行执行的,互不依赖

避坑指南

FindXXX.cmake 时,有几个坑我踩过,分享给你:

  • 路径写死:别把路径写死在模块里。用 PATHS 参数列出常见路径,或者让用户通过 XXX_ROOT 环境变量指定。
  • 忘记处理版本号:有些库有多个版本,比如 libxxx.so.1.2.3。用 find_libraryNAMES_PER_DIR 选项可以逐个目录查找。
  • 忽略跨平台:Windows 下库文件叫 xxx.lib,Linux 下叫 libxxx.so。在 NAMES 里把两种都写上。
  • 不提供导入目标:老式的 XXX_INCLUDE_DIRSXXX_LIBRARIES 虽然能用,但不如导入目标方便。建议两者都提供。
我的习惯: 我会在模块开头加一段注释,写明这个模块支持哪些版本、依赖哪些其他库。这样半年后自己回来看,也能快速上手。

总结

FindXXX.cmake 其实不难,核心就是 find_pathfind_library 两个命令。加上导入目标,就能让使用者像用官方库一样方便。

下次你遇到一个没有 CMake 支持的库,不妨花十分钟写个查找模块。相信我,这十分钟的投入,会在未来无数次 cmake .. 中回报你。


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