最佳实践:命名规范、最小权限原则、避免全局变量、文档化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 |
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
)
用PRIVATE、PUBLIC、INTERFACE来控制可见性。说白了:
- 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分钟。
三、避免全局变量:用函数和属性代替
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
)
这样做的好处是:每个目标都是独立的,依赖关系清晰可见。就算某个子目录删掉了,也不会影响其他模块。
四、文档化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"
)
这样做的好处是——半年后你回来维护,看到注释就能想起来当时为什么这么写。
五、知识体系总览
下面这张图,把今天讲的四个最佳实践串起来了:
六、总结
这四个最佳实践,说白了就是一句话:把CMakeLists.txt当成代码来写。
- 命名规范:让变量名自己会说话,不用猜
- 最小权限原则:只给目标真正需要的东西,别过度暴露
- 避免全局变量:用目标和依赖关系代替全局状态
- 文档化:写给人看的注释,比写给机器看的代码更重要
我做了这么多年嵌入式项目,发现一个规律:项目前期多花10分钟规范CMakeLists.txt,后期能省下10小时的排查时间。这笔账,怎么算都划算。
公众号:蓝海资料掘金营,微信deep3321