Modern CMake 学习笔记:从 target 思维到多平台组织

2023 年 8 月 6 日 星期日
/ , ,
4
摘要
基于 Modern CMake 中文指南的学习笔记,以 ArcDetect_Timestamps 项目为例。涵盖 target 思维、option 平台分支、PUBLIC/PRIVATE/INTERFACE、主项目守卫、子项目组织。

阅读此文章之前,你可能需要首先阅读以下的文章才能更好的理解上下文。

Modern CMake 学习笔记:从 target 思维到多平台组织

编写时间:2023-08

学习 Modern CMake 的主要参考是 Modern CMake 中文指南。这篇文章不是在翻译它,而是在时间戳检测项目里实际用到的一些写法和工作方式,记录下来的学习笔记。

核心转变:从全局到 target

老式 CMake 习惯用 include_directorieslink_librariesadd_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() 里的 VERSIONDESCRIPTION 也不是装饰,其他 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、OpenCV
  • Jetson/:构建 det_trt,链接 TensorRT、CUDA、OpenCV

顶层只做组装,不替子目录声明依赖——这是 Modern CMake 提倡的"关注点分离"。

工程直觉

几条在实践中反复验证的经验:

  1. 宁可用 target_* 多写几行,也不要图省事用全局命令。全局命令在项目变大的时候会反噬。
  2. PRIVATE 是默认的安全选项。不确定时先用 PRIVATE,等确实需要暴露时再改成 PUBLIC,比反过来安全。
  3. 平台分支用 option()
    • target_compile_definitions
    ,比在代码里 #ifdef 猜平台更显式。
  4. 子目录各自独立,顶层 CMake 只做组装,不替子目录声明依赖。
  5. CMake 是声明式,不是脚本:描述 target 要什么,不是描述怎么构建。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...