4. CMake 构建系统:从入门到实战

嵌入式开发里,构建系统这事儿,说大不大,说小不小。我见过太多团队,代码写得挺好,结果卡在编译环境上,一折腾就是半天。CMake 不是万能的,但用好了,确实能省下大把时间。

这一章,咱们就聊聊 CMakeLists.txt 怎么写、库怎么生成和链接、第三方依赖怎么管,还有交叉编译怎么配。嗯,都是实战里绕不开的东西。

4.1 CMakeLists.txt 基础结构

说白了,CMakeLists.txt 就是告诉 CMake 怎么编译你的代码。一个最简单的项目,几行就能搞定:

cmake_minimum_required(VERSION 3.10)
project(NavigationDemo)

add_executable(nav_demo main.c)

但实际项目哪有这么简单?你想想看,导航算法里通常有几十个源文件,还有各种依赖库。这时候就得把结构理清楚。

我个人习惯,项目根目录放一个顶层 CMakeLists.txt,每个子模块再放一个。这样层次分明,改起来也方便。

顶层 CMakeLists.txt 示例:

cmake_minimum_required(VERSION 3.10)
project(NavigationSystem VERSION 1.0.0 LANGUAGES C CXX)

# 设置 C 标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# 添加子目录
add_subdirectory(src)
add_subdirectory(lib)
add_subdirectory(test)

4.2 库的生成与链接

导航系统里,很多算法模块会被多个可执行文件共用。比如卡尔曼滤波、坐标转换这些,做成库最合适。

CMake 支持两种库:静态库和动态库。静态库编译时直接打进可执行文件,动态库运行时加载。嵌入式场景下,我建议优先用静态库——省去动态加载的麻烦,也避免目标板上缺库文件。

库类型 CMake 命令 适用场景
静态库 add_library(kalman STATIC kalman.c) 嵌入式、资源受限、单进程
动态库 add_library(kalman SHARED kalman.c) 桌面应用、插件系统、多进程

链接库的时候,用 target_link_libraries 就行。注意顺序——被依赖的库要放在后面。

add_executable(nav_demo main.c)
target_link_libraries(nav_demo kalman coordinate_transform)

小技巧:用 target_include_directories 指定头文件路径,比全局的 include_directories 更干净。每个目标只管自己的头文件,不会污染其他模块。

4.3 第三方依赖管理

导航系统经常要用到第三方库,比如 Eigen(矩阵运算)、Boost(几何计算)、或者自家的底层驱动库。怎么优雅地引入这些依赖?

CMake 提供了 find_package 命令。它会去系统路径或者你指定的路径下找库的配置文件。

find_package(Eigen3 REQUIRED)
find_package(Boost REQUIRED COMPONENTS geometry)

if(Eigen3_FOUND)
  target_link_libraries(nav_demo Eigen3::Eigen)
endif()

但嵌入式环境里,系统上不一定装了这些库。这时候我建议用 FetchContent 模块——它能在编译时自动下载源码并编译,省去手动安装的麻烦。

FetchContent 示例:

include(FetchContent)

FetchContent_Declare(
  eigen
  GIT_REPOSITORY https://gitlab.com/libeigen/eigen.git
  GIT_TAG 3.4.0
)

FetchContent_MakeAvailable(eigen)

我曾经在一个项目里,团队五个人,每个人装第三方库的方式都不一样。有人用 apt,有人手动编译,还有人直接从同事那拷 .so 文件。后来统一用 FetchContent,再也没出过「我机器上能编,你那边不行」的问题。

注意:FetchContent 会下载源码到 build 目录。如果网络不好,或者库很大,第一次编译会慢一些。建议把下载好的源码缓存起来,避免每次 clean build 都重新下载。

4.4 交叉编译配置

嵌入式开发绕不开交叉编译。你在 x86 的电脑上写代码,编译出来的程序要在 ARM 或者 RISC-V 的板子上跑。CMake 处理这个,靠的是 工具链文件。

工具链文件就是一个 .cmake 文件,里面指定了编译器、链接器、目标架构等信息。我一般把它放在项目根目录的 cmake/ 文件夹下。

# arm-none-eabi-gcc.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

编译的时候,通过 -DCMAKE_TOOLCHAIN_FILE 指定这个文件:

cmake -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi-gcc.cmake ..

嗯,这里要注意:工具链文件里 CMAKE_FIND_ROOT_PATH_MODE 那几行很关键。它告诉 CMake,找库和头文件的时候,只在交叉编译的 sysroot 里找,别去宿主系统里翻。否则很容易链接到 x86 的库,编译不报错,运行就崩。

避坑指南:我曾经在交叉编译时,忘记设置 CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER,结果 CMake 找到了宿主系统的 pkg-config,传了一堆 x86 的链接参数回来。编译通过,但链接出来的二进制在板子上直接 segfault。排查了整整一天。

4.5 本章知识体系

下面这张图,把 CMake 构建系统的核心逻辑串起来了。从顶层 CMakeLists.txt 开始,到库的生成、依赖管理、交叉编译,每一步都有对应的 CMake 命令和配置。

CMake 构建系统核心逻辑 顶层 CMakeLists.txt 库的生成 (add_library) 依赖管理 (find_package) 交叉编译 (工具链文件) STATIC / SHARED target_link_libraries FetchContent / 系统库 target_include_directories CMAKE_TOOLCHAIN_FILE CMAKE_FIND_ROOT_PATH_MODE 生成可执行文件 / 库文件 导航系统二进制 (.elf / .bin) 交叉编译时,工具链文件决定编译器;库的生成与链接决定模块复用;依赖管理决定第三方库的引入方式 三者配合,才能构建出可运行的导航系统固件

4.6 实战建议

说了这么多,最后给几条实在的建议:

  • 版本号写清楚:cmake_minimum_required 别偷懒,写个具体版本。我见过有人写 VERSION 2.8,结果新特性用不了,排查半天才发现是 CMake 版本太老。
  • 用 target_ 系列命令:target_include_directories、target_compile_definitions 这些,比全局命令更安全。每个目标只管自己的事,不会互相干扰。
  • 交叉编译先验证:拿到工具链文件后,先编译一个 hello world 试试。确认编译器、链接器、sysroot 都对了,再编译大项目。否则出了问题,你都不知道是代码问题还是工具链问题。
  • 缓存变量用起来:像 CMAKE_BUILD_TYPE、CMAKE_INSTALL_PREFIX 这些,用 set(CACHE ...) 设置默认值,方便团队成员统一配置。

CMake 这东西,初看觉得啰嗦,用久了就会发现它的好。结构清晰、跨平台、可复用——对于导航系统这种需要长期维护的项目来说,值得花时间把构建系统搭好。