最佳实践:命名规范、最小权限原则、避免全局变量、文档化CMakeLists.txt

做嵌入式CMake项目,时间一长你就会发现:代码腐烂的速度,往往取决于CMakeLists.txt的混乱程度。我见过太多项目,一开始随便写写,三个月后连作者自己都看不懂那些变量是干嘛的。

今天聊的这四个最佳实践,说白了就是给CMake项目“立规矩”。规矩立好了,后期维护成本能降一半以上。

一、命名规范:让变量名自己会说话

我个人习惯,CMake里的命名要遵循“一看就懂,不用猜”的原则。你想想看,一个变量叫SRC,到底是源文件列表?还是某个源文件的路径?

1.1 变量命名规则

类别 推荐格式 示例
源文件列表 ${PROJECT}_SRCS MY_BOOT_SRCS
头文件路径 ${PROJECT}_INCLUDE_DIRS MY_BOOT_INCLUDE_DIRS
链接库 ${PROJECT}_LIBS MY_BOOT_LIBS
编译选项 ${PROJECT}_COMPILE_OPTIONS MY_BOOT_COMPILE_OPTIONS
目标名称 ${PROJECT}_${COMPONENT} my_boot_hal
我的小技巧:所有变量名统一用大写+下划线,目标名用小写+下划线。这样在CMakeLists.txt里扫一眼,就能分清“变量”和“目标”。

1.2 函数和宏的命名

函数名我建议用项目名_功能名的格式。比如:

function(my_boot_add_driver driver_name)
    # 添加驱动的通用逻辑
endfunction()

这样做的好处是——不会跟其他第三方模块的函数重名。我在项目中遇到过两次,因为函数名冲突导致CMake配置失败,排查起来特别痛苦。

二、最小权限原则:别给目标不需要的东西

这个原则是从安全领域借来的,但在CMake里同样适用。每个目标只应该拥有它真正需要的权限和依赖

2.1 头文件路径的“粒度控制”

很多人喜欢这么写:

include_directories(.)  # 把整个项目目录都加进去

嗯,这里要注意——这是典型的“懒人写法”。一旦项目变大,你根本不知道哪个目标偷偷依赖了不该依赖的头文件。

我建议改成:

target_include_directories(my_boot_hal
    PRIVATE
        ${CMAKE_CURRENT_SOURCE_DIR}/inc
        ${CMAKE_CURRENT_SOURCE_DIR}/../common/inc
)

PRIVATEPUBLICINTERFACE来控制可见性。说白了:

  • PRIVATE:只有自己用,不传给依赖者
  • PUBLIC:自己用,也传给依赖者
  • INTERFACE:自己不用,只传给依赖者(比如纯头文件库)

2.2 链接库的权限控制

同样的道理:

target_link_libraries(my_boot_app
    PRIVATE
        my_boot_hal
        my_boot_os
    PUBLIC
        my_boot_common
)

我曾经接手过一个项目,所有库都是PUBLIC链接,结果一个底层驱动的改动,导致上层三个应用模块重新编译——编译时间从5分钟变成了40分钟

避坑指南:我曾经因为图省事,把所有依赖都设成PUBLIC,结果项目后期每次改一个底层头文件,整个项目都要重新编译。后来花了两天时间重构,才把编译时间降下来。

三、避免全局变量:用函数和属性代替

CMake里的全局变量,就像C语言里的全局变量一样——方便一时,痛苦一世

3.1 为什么不要用全局变量?

假设你在根CMakeLists.txt里定义了:

set(ALL_SRCS "")

然后在各个子目录里:

set(ALL_SRCS "${ALL_SRCS};${CMAKE_CURRENT_SOURCE_DIR}/src/main.c" PARENT_SCOPE)

你想想看,如果某个子目录忘记加PARENT_SCOPE,或者拼写错了变量名,排查起来有多崩溃?

3.2 推荐做法:用目标属性

我现在的做法是:每个子目录只创建自己的目标,然后通过依赖关系传递

# drivers/CMakeLists.txt
add_library(my_boot_drivers STATIC
    src/gpio.c
    src/uart.c
    src/spi.c
)

target_include_directories(my_boot_drivers
    PUBLIC inc
)

然后在主CMakeLists.txt里:

add_subdirectory(drivers)
add_subdirectory(hal)
add_subdirectory(app)

target_link_libraries(my_boot_firmware
    PRIVATE
        my_boot_drivers
        my_boot_hal
        my_boot_app
)

这样做的好处是:每个目标都是独立的,依赖关系清晰可见。就算某个子目录删掉了,也不会影响其他模块。

核心思想:用目标(target)代替变量,用依赖关系代替全局状态。这是CMake现代化的关键一步。

四、文档化CMakeLists.txt:写给人看的构建脚本

很多人觉得CMakeLists.txt是写给CMake看的,能跑就行。但我想说——CMakeLists.txt首先是写给人看的

4.1 文件头注释

每个CMakeLists.txt开头,我习惯加一段注释:

# ============================================================
# 模块名称: HAL层构建脚本
# 功能描述: 编译硬件抽象层,包括GPIO、UART、SPI驱动
# 依赖关系: 依赖drivers模块,被app模块依赖
# 作者: Zhang San
# 最后修改: 2024-01-15
# ============================================================

4.2 变量和函数的文档

对于复杂的变量或自定义函数,加注释说明:

# 编译选项:开启硬件浮点运算
# 注意:只有Cortex-M4F及以上内核才支持
set(HAL_FPU_OPTIONS "-mfloat-abi=hard -mfpu=fpv4-sp-d16")

# 自定义函数:添加一个驱动模块
# 参数:
#   driver_name - 驱动名称,如gpio、uart
#   src_files   - 源文件列表
function(add_hal_driver driver_name src_files)
    # ...
endfunction()

4.3 用注释标记“坑”

我在项目中遇到过一些奇怪的编译问题,都会在CMakeLists.txt里留下注释:

# 【坑】STM32F4的HAL库在-O2优化下,SPI中断会丢失
# 临时解决方案:关闭SPI相关文件的优化
set_source_files_properties(
    ${CMAKE_CURRENT_SOURCE_DIR}/src/spi.c
    PROPERTIES COMPILE_FLAGS "-O0"
)

这样做的好处是——半年后你回来维护,看到注释就能想起来当时为什么这么写

五、知识体系总览

下面这张图,把今天讲的四个最佳实践串起来了:

CMake最佳实践知识体系 CMake最佳实践 命名规范 最小权限原则 避免全局变量 文档化 变量名:大写+下划线 目标名:小写+下划线 函数名:项目名_功能名 PRIVATE/PUBLIC/INTERFACE 头文件路径粒度控制 链接库权限控制 用目标代替变量 依赖关系传递 避免PARENT_SCOPE滥用 文件头注释 变量/函数文档 标记“坑”和解决方案 目标:可维护、可理解、可复用的CMake构建系统

六、总结

这四个最佳实践,说白了就是一句话:把CMakeLists.txt当成代码来写

  • 命名规范:让变量名自己会说话,不用猜
  • 最小权限原则:只给目标真正需要的东西,别过度暴露
  • 避免全局变量:用目标和依赖关系代替全局状态
  • 文档化:写给人看的注释,比写给机器看的代码更重要

我做了这么多年嵌入式项目,发现一个规律:项目前期多花10分钟规范CMakeLists.txt,后期能省下10小时的排查时间。这笔账,怎么算都划算。

最后送大家一句话:好的CMakeLists.txt,应该是“新人接手后,不用问你就能看懂”的。

公众号:蓝海资料掘金营,微信deep3321