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