Modern CMake 学习笔记:从 target 思维到多平台组织
编写时间:2023-08
学习 Modern CMake 的主要参考是 Modern CMake 中文指南。这篇文章不是在翻译它,而是在时间戳检测项目里实际用到的一些写法和工作方式,记录下来的学习笔记。
核心转变:从全局到 target
老式 CMake 习惯用 include_directories、link_libraries、add_definitions 这些全局操作。Modern CMake 的核心变动是把一切挂在 target 上:
| 老写法 | Modern 写法 |
|---|---|
include_directories(path) | target_include_directories(target PRIVATE path) |
link_libraries(lib) | target_link_libraries(target PRIVATE lib) |
add_definitions(-DFOO) | target_compile_definitions(target PRIVATE FOO=1) |
直觉很简单:一个 target 应该自己声明自己需要什么,而不是依赖全局状态。全局操作会让大项目变得不可预测——你不知道哪个 target 悄悄继承了哪个 include 路径。
版本范围和 project 声明
cmake_minimum_required(VERSION 3.10...3.21)
project(
ArcDetect_Timestamps
VERSION 1.0
DESCRIPTION "Arc Detect Project"
LANGUAGES CXX
)VERSION 3.10...3.21 表示"在 3.10 到 3.21 之间都能工作",比写死一个最低版本更诚实——它告诉 CMake 也告诉读代码的人:这个项目的 CMake 特性不会超过 3.21。project() 里的 VERSION、DESCRIPTION 也不是装饰,其他 CMake 命令和打包工具会用到。
option 控制平台分支
时间戳检测需要同时在 RK3588 和 Jetson 上编译,但两边的推理后端完全不同。用 option() 让用户在 cmake 命令行选择:
option(BUILD_FOR_RK "Build for RK platform" OFF)
option(BUILD_FOR_Jetson "Build for Jetson platform" ON)
if(BUILD_FOR_RK)
add_subdirectory(RK)
elseif(BUILD_FOR_Jetson)
add_subdirectory(Jetson)
endif()然后库本身用 target_compile_definitions 把平台宏传给预处理器:
if(BUILD_FOR_RK)
target_compile_definitions(ArcDetect PRIVATE PLATFORM_RK=1)
target_link_libraries(ArcDetect PRIVATE det_rknn)
elseif(BUILD_FOR_Jetson)
target_compile_definitions(ArcDetect PRIVATE PLATFORM_JETSON=1)
target_link_libraries(ArcDetect PRIVATE det_trt)
endif()注意 PRIVATE:平台宏只影响 ArcDetect 自己的编译,不会泄漏给依赖它的 target。
主项目守卫
CMake 项目可能被 add_subdirectory 作为子项目引入。这时候有些设置不该重复执行:
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
set(CMAKE_CXX_EXTENSIONS OFF)
set_property(GLOBAL PROPERTY USE_FOLDERS ON)
endif()CMAKE_PROJECT_NAME 是顶层项目名,PROJECT_NAME 是当前最近一次 project() 的名称。两者相等时,说明当前项目就是顶层——这时候才做全局设置。同样的守卫也用在 demo:
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
add_executable(ArcDetect_demo demo/demo.cpp)
target_link_libraries(ArcDetect_demo PRIVATE ArcDetect)
endif()PUBLIC vs PRIVATE vs INTERFACE
这个区分是 Modern CMake 里最需要形成直觉的部分:
| 关键字 | 谁需要 | 典型场景 |
|---|---|---|
PRIVATE | 只自己编译需要 | 平台宏、内部实现细节 |
PUBLIC | 自己和依赖者都需要 | 头文件里暴露的类型来自第三方库 |
INTERFACE | 只依赖者需要 | header-only 库、纯接口 target |
时间戳项目里,公开头文件 ArcDetect.h 的 include 目录用 PUBLIC:
target_include_directories(ArcDetect PUBLIC ${CMAKE_SOURCE_DIR}/include)而 RK 平台的头文件只在自己的 .cc 里用到,用 PRIVATE:
target_include_directories(ArcDetect PRIVATE ${RKNN_DIR}/include)子项目的 target 组织
子目录 RK/ 和 Jetson/ 各自构建自己的推理库,顶层只管选择谁、连接谁。子目录自己管自己的头文件路径和第三方依赖:
RK/:构建det_rknn,链接 RKNN Runtime、RGA、OpenCVJetson/:构建det_trt,链接 TensorRT、CUDA、OpenCV
顶层只做组装,不替子目录声明依赖——这是 Modern CMake 提倡的"关注点分离"。
工程直觉
几条在实践中反复验证的经验:
- 宁可用
target_*多写几行,也不要图省事用全局命令。全局命令在项目变大的时候会反噬。 PRIVATE是默认的安全选项。不确定时先用 PRIVATE,等确实需要暴露时再改成 PUBLIC,比反过来安全。- 平台分支用
option()target_compile_definitions
#ifdef猜平台更显式。 - 子目录各自独立,顶层 CMake 只做组装,不替子目录声明依赖。
- CMake 是声明式,不是脚本:描述 target 要什么,不是描述怎么构建。