3. CMake进阶:find_package机制、FetchContent与ExternalProject、自定义Target与命令、工具链文件

各位同学,咱们今天聊点硬核的。CMake 这东西,很多人觉得就是个构建脚本生成器,写几个 add_executable 就完事了。但说实话,那只是冰山一角。真正到了跨平台项目里,你会发现依赖管理、外部库引入、自定义构建步骤,才是让人头疼的地方。

我个人习惯把 CMake 分成三个阶段:能用、会用、玩得转。今天讲的这些,就是帮你从「会用」跨到「玩得转」的关键。

3.1 find_package:找库的正确姿势

先说说 find_package。这玩意儿说白了就是帮你在系统里找别人写好的库。比如你要用 OpenCV、Boost、Qt,总不能让人家手动去配路径吧?

find_package 有两种模式:Module 模式和Config 模式。

  • Module 模式:CMake 自带一堆 FindXXX.cmake 脚本,比如 FindBoost.cmake。它会去标准路径下找头文件和库文件。
  • Config 模式:库的作者自己提供了 XXXConfig.cmake 或 xxx-config.cmake。这种方式更可靠,因为库知道自己装在了哪里。

举个例子,找 OpenCV:

find_package(OpenCV REQUIRED)
if(OpenCV_FOUND)
    include_directories(${OpenCV_INCLUDE_DIRS})
    target_link_libraries(my_app ${OpenCV_LIBS})
endif()

这里有个坑,我曾经踩过——REQUIRED 关键字。如果你加了它,找不到库就直接报错终止。有时候你只是想「试试看能不能找到」,那就别加 REQUIRED,用 QUIET 或者直接不写。

注意:不同版本的 CMake,find_package 的行为有细微差别。比如 CMake 3.15 之后,find_package 默认会优先走 Config 模式。如果你发现某个库死活找不到,先检查一下 CMake 版本。

3.2 FetchContent:现代 CMake 的依赖管理利器

以前我们怎么处理第三方依赖?要么让用户自己装,要么把源码拷到项目里。这两种方式都不太优雅。

FetchContent 是 CMake 3.11 引入的模块。它可以在构建时自动下载、配置、构建外部项目。说白了,就是 CMake 版的包管理器。

我最近做一个跨平台项目,需要用到 Google Test。用 FetchContent 简直不要太爽:

include(FetchContent)

FetchContent_Declare(
    googletest
    GIT_REPOSITORY https://github.com/google/googletest.git
    GIT_TAG release-1.12.0
)

FetchContent_MakeAvailable(googletest)

# 然后直接链接
target_link_libraries(my_test gtest_main)

你看,三行代码搞定。不需要用户提前装 GTest,也不需要你手动管理源码。构建的时候自动下载,自动编译,自动链接。

小技巧:如果你在公司内网,或者担心 GitHub 访问慢,可以先用 FetchContent_Declare 的 URL 参数指定一个镜像地址。我个人习惯在项目根目录放一个 cmake/DownloadMirrors.cmake,统一管理所有外部依赖的下载源。

3.3 ExternalProject:更底层的控制

ExternalProject 比 FetchContent 更老,也更灵活。它允许你对外部项目进行更精细的控制,比如指定构建工具、配置参数、甚至打补丁。

什么时候用 ExternalProject?我举个例子:你依赖一个用 Autotools 构建的 C 库,或者需要给第三方库传一些特殊的编译选项。这时候 FetchContent 可能不够用。

include(ExternalProject)

ExternalProject_Add(
    my_external_lib
    GIT_REPOSITORY https://github.com/someone/somelib.git
    GIT_TAG v1.0
    CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
    PATCH_COMMAND patch -p1 < ${CMAKE_SOURCE_DIR}/patches/fix.patch
)

# 注意:ExternalProject 是构建时下载,不是配置时
# 所以你不能直接在 target_link_libraries 里用它的目标
# 需要手动指定路径

这里有个关键区别:FetchContent 是配置时执行,下载和构建发生在 CMake 的 configure 阶段;ExternalProject 是构建时执行,发生在 make 或 ninja 阶段。这个区别决定了你怎么使用它们。

3.4 自定义 Target 与命令

有时候,光编译代码不够。你可能需要生成一些文件、运行脚本、或者做代码检查。这时候就需要自定义 target 了。

add_custom_target 和 add_custom_command 是 CMake 提供的两个利器。

  • add_custom_target:创建一个没有输出文件的 target,通常用于执行一些操作,比如运行测试、生成文档。
  • add_custom_command:为某个 target 添加自定义构建步骤,比如在编译前生成头文件。

我做过一个项目,需要在编译前根据配置文件生成版本号头文件。用 add_custom_command 完美解决:

add_custom_command(
    OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.h
    COMMAND ${CMAKE_COMMAND} -E echo "#define VERSION \"1.0.0\"" > ${CMAKE_CURRENT_BINARY_DIR}/version.h
    DEPENDS ${CMAKE_SOURCE_DIR}/VERSION.txt
    COMMENT "Generating version.h"
)

add_executable(my_app main.cpp ${CMAKE_CURRENT_BINARY_DIR}/version.h)
target_include_directories(my_app PRIVATE ${CMAKE_CURRENT_BINARY_DIR})

嗯,这里要注意:add_custom_command 的 OUTPUT 必须是一个实际生成的文件,否则 CMake 会认为这个命令永远不需要执行。

3.5 工具链文件:跨平台编译的灵魂

终于说到工具链文件了。这东西是跨平台编译的核心,但很多人觉得它很神秘。

说白了,工具链文件就是告诉 CMake:你用哪个编译器?编译器在哪?目标平台是什么?

比如你要交叉编译到 ARM 平台:

# arm-toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)

set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

使用的时候:

cmake -DCMAKE_TOOLCHAIN_FILE=arm-toolchain.cmake ..

我曾经犯过一个错误:忘记设置 CMAKE_FIND_ROOT_PATH_MODE_PROGRAM 为 NEVER。结果 CMake 在宿主机上找到了 pkg-config,但拿到的库路径却是宿主机的,导致链接失败。这个坑,你记住就好。

核心要点:工具链文件里,CMAKE_SYSTEM_NAME 和 CMAKE_SYSTEM_PROCESSOR 决定了 CMake 认为你在为哪个平台编译。这两个变量一旦设置,CMake 就会自动切换到交叉编译模式。

知识体系总览

下面这张图,是我自己总结的 CMake 进阶知识体系。你可以把它当作一个思维导图来看:

CMake 进阶 find_package • Module 模式 (FindXXX.cmake) • Config 模式 (XXXConfig.cmake) • REQUIRED / QUIET 关键字 FetchContent • 配置时下载 & 构建 • 支持 Git / URL / SVN • 现代 CMake 推荐方式 ExternalProject • 构建时下载 & 构建 • 支持打补丁、自定义命令 • 更底层的控制 自定义 Target • add_custom_target • add_custom_command • 生成文件 / 运行脚本 工具链文件 • 指定编译器 & 目标平台 • 交叉编译的核心 • CMAKE_TOOLCHAIN_FILE

总结一下

今天讲的这几个东西,说白了就是 CMake 进阶路上的五个关卡:

  1. find_package:找别人写好的库,省时省力。
  2. FetchContent:现代 CMake 的依赖管理,推荐优先使用。
  3. ExternalProject:当你需要更精细的控制时,用它。
  4. 自定义 Target:让构建过程做更多事情,不只是编译。
  5. 工具链文件:跨平台编译的基石,理解它你才能真正驾驭 CMake。

我个人觉得,工具链文件是最值得花时间研究的。你想想看,一个项目能在 Windows、Linux、macOS 甚至嵌入式设备上无缝编译,那种成就感,嗯,很爽。

好了,今天就到这里。记住,CMake 不是魔法,它只是一套规则。你理解了规则,就能玩转它。

课后思考:如果你有一个项目,既需要 FetchContent 下载 Google Test,又需要 ExternalProject 编译一个古老的 C 库,你会怎么组织 CMakeLists.txt?试试看,把这两个结合起来。
可可精品资料库