CMAKE 的合理化结构
关于
🎉终于终于,在 keil 的绿尸寒压力下,公司准备转型到 gcc 编译。而我觉得 gcc 编译链的整合应该没有比 CMAKE 更好的方案(当然 EIDE 插件也不错),长久独立探索,终于彻底把 keil 踢掉了。eclipse 还无法完全踢掉,毕竟 TI 的 MCU 还得 CCS 来,什么时候能实现 vscode 全面普及呢?
最近把原本的项目工程转成完全的 CMAKE 工程,发现一些可以优化的工程结构的点。
工具
在博客文件服务页应该有便携 vscode 的下载,各种需要的工具都集成进去了,绿色免安装。
有一个 1_首次打开请运行.bat 的文件,双击运行一下,vscode 就可以直接用了。这个脚本的主要作用是,提权到管理员模式,把各个工具链的 path 自动添加到环境变量里。
vscode 工作区的合理化
在 .code-workspace 文件的 settings 段可以增加如下的部分
"settings": {
"C_Cpp.formatting": "clangFormat",
"cmake.configureEnvironment": {
"PROGRAM_NAME": "H7_GCC_PIONEER"
}
},
其中指定了格式化程序,是 clangFormat ,这个格式化程序的可执行文件会在 1_首次打开请运行.bat 时增加到环境变量里。
指定了一个 CMAKE 的环境变量 ,这里是为了方便配置生成的烧录文件的名字。
在主 cmakelist 也就是根目录下,此前有这样的内容
set(CMAKE_PROJECT_NAME DebugBuild)
修改为如下的部分
if(DEFINED ENV{PROGRAM_NAME})
set(CMAKE_PROJECT_NAME $ENV{PROGRAM_NAME})
else()
message(WARNING "PROGRAM_NAME environment variable is not set. Using default project name.")
set(CMAKE_PROJECT_NAME "DefaultProjectName")
endif()
这样就会以工作区 .code-workspace
文件中的 PROGRAM_NAME 为准了。
这样操作主要是为了开发这个项目的人尽可能不要修改 CMAKE 相关的任何文件,后续还有以这个为目的的其他操作。
主构建的分支
对于 CMAKE_BUILD_TYPE ,会从 CMAKE 插件传入实际构建。但除了这个构建类型,还希望有一些自定义的构建类型,可以进行如下的操作。
在可执行主构建较为靠前的地方使用自定义的属性变量。
set(CMAKE_BUILD_BL "YES")
set(CMAKE_BUILD_BL "NO")
而在工具链指定文件中我们可以这样操作(当然其他文件中也都类似,主构建的这些属性变量会传播到子目录和子构建中)
if(CMAKE_BUILD_BL MATCHES YES)
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/CPU/GNU/stm32H743XI_INCBL.ld\"") # 添加链接脚本
endif()
if(CMAKE_BUILD_BL MATCHES NO)
set(CMAKE_C_LINK_FLAGS "${CMAKE_C_LINK_FLAGS} -T \"${CMAKE_SOURCE_DIR}/CPU/GNU/stm32H743XI.ld\"") # 添加链接脚本
endif()
这样操作控制了两个不同的链接器脚本,YES 时的链接器脚本具备 bootload 部分,主要用于出厂烧录;NO 时则不包括 bootload 部分,用于升级。这样又减少了一部分操作,EIDE 插件却做不到这么方便,每次需要手动小改一下脚本。当然为了尽可能不让开发者修改 CMAKE 相关的任何文件,也可以如上一个操作一样新建一个环境变量。
主构建中定义后任务
我的主构建最末尾有如下两句
# 为单独的文件添加编译标签
include("${CMAKE_CURRENT_LIST_DIR}/8_WorkSpace/CMake/toolCmake/extra-compile-flags.cmake")
# 运行一下构建后任务
include("${CMAKE_CURRENT_LIST_DIR}/8_WorkSpace/CMake/toolCmake/post-build-tasks.cmake")
extra-compile-flags.cmake
这个文件的内容如下
include(${CMAKE_CURRENT_LIST_DIR}/cmake_func/functions.cmake)
set_compile_flags_for_matching_files(${CMAKE_PROJECT_NAME} "6_Rtos|7_Exlib" "-w")
set_compile_flags_for_matching_files(user_src "6_Rtos|7_Exlib" "-w")
作用是将主构建工程 ${CMAKE_PROJECT_NAME}
以及源码引入工程 user_src
中关于 6_Rtos 或 7_Exlib 文件夹下的源码都添加 -w
的编译标志,意为忽略所有警告。因为我们开启的超严格全警告,所以操作系统或者别人写的一些库中或许会有一些警告,非常烦人又不能改,故有这样的操作。注意的是,由于源码引入工程 user_src
原本是所有部分都以 INTERFACE 属性定义,所以在 user_src
中的源码无法正常添加编译标志,所以 target_sources
部分现在应该以 PUBLIC 属性定义,暂未发现无法接受的副作用。
第一个传参是项目名,第二个传参是指路径包含这些字段的源码文件,第三个字段是期望添加的编译标签,这又引入新文件了 functions.cmake 如下。
# FUNC
# 递归包含头文件的函数
function(include_sub_directories_recursively root_dir)
if (IS_DIRECTORY ${root_dir}) # 当前路径是一个目录吗,是的话就加入到包含目录
# if (${root_dir} MATCHES "include")
message("include dir: " ${root_dir})
target_include_directories(${PROJECT_NAME} INTERFACE
${root_dir}
)
# endif()
endif()
file(GLOB ALL_SUB RELATIVE ${root_dir} ${root_dir}/*) # 获得当前目录下的所有文件,让如 ALL_SUB 列表中
foreach(sub ${ALL_SUB})
if (IS_DIRECTORY ${root_dir}/${sub})
include_sub_directories_recursively(${root_dir}/${sub}) # 对子目录递归调用,包含
endif()
endforeach()
endfunction()
# 给某个目标,路径带有关键词的源文件,添加期望添加的标签(例如"-w")
function(set_compile_flags_for_matching_files target_name keywords compile_flags)
# 获取目标的原始源文件列表
get_target_property(src_list ${target_name} SOURCES)
# 检查目标是否存在并有源文件
if(NOT src_list)
message(WARNING "Target '${target_name}' does not exist or has no sources.")
return()
endif()
# 创建一个新的列表用于存放需要设置编译标志的文件
set(filtered_src_list)
# 遍历原始的源文件列表,筛选出需要的文件
foreach(src_file IN LISTS src_list)
if("${src_file}" MATCHES "${keywords}")
list(APPEND filtered_src_list ${src_file})
endif()
endforeach()
# 为筛选出的源文件设置编译标志
foreach(src_file IN LISTS filtered_src_list)
set_source_files_properties(${src_file} PROPERTIES COMPILE_FLAGS "${compile_flags}")
endforeach()
endfunction()
# FUNC END
这是一个我专用来编写 cmake 函数的文件,不过多介绍,反正需要用到函数的时候就会 include 。
post-build-tasks.cmake
这个文件是为了方便管理编译后任务,内容如下
# 单独管理构建后的任务
# 定义工具链工具
set(OBJCOPY arm-none-eabi-objcopy)
set(OBJDUMP arm-none-eabi-objdump)
# 添加自定义命令生成 bin 和 hex 文件
add_custom_command(TARGET ${CMAKE_PROJECT_NAME} POST_BUILD
COMMAND ${OBJCOPY} -O binary $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_PROJECT_NAME}.bin
COMMAND ${OBJCOPY} -O ihex $<TARGET_FILE:${CMAKE_PROJECT_NAME}> ${CMAKE_PROJECT_NAME}.hex
COMMAND ${CMAKE_CURRENT_LIST_DIR}/gccMapView.exe ${CMAKE_BINARY_DIR}
COMMENT "Generating bin and hex files from elf"
)
这个是为了生成 bin 文件和 hex 文件。 随后运行了一下 gccMapView.exe ,这是我为了整理 map 做的小软件,因为 GCC 的原版 map 简直丑陋所以有这个小软件,开源地址在这里,效果还是非常不错的。
工程源码引入使用遍历搜索自动化
工程源码遍历头文件路径并且包含,有这样的部分
# cmake FUNC
include(${CMAKE_CURRENT_LIST_DIR}/cmake_func/functions.cmake)
# 递归包含头文件
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../1_App)
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../2_Contorl)
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../3_Module)
include_sub_directories_recursively(${CMAKE_CURRENT_LIST_DIR}/../../../4_Driver)
这会调用到 cmake 函数,所以也在统一函数定义文件中,作用就是将这四个文件夹下的所有路径包括子目录统统添加 include path 。
又有这样的部分
# 递归查找所有源码文件
file(GLOB_RECURSE 1_APP_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../1_App/*.c)
file(GLOB_RECURSE 2_CONTORL_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../2_Contorl/*.c)
file(GLOB_RECURSE 3_MODULE_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../3_Module/*.c)
file(GLOB_RECURSE 4_DRIVER_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../4_Driver/*.c)
file(GLOB_RECURSE 5_DRIVER_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../5_PhysicalChip/Stm32H7/*.c)
target_sources(${CMAKE_PROJECT_NAME} PUBLIC
../../../main.c
${1_APP_SRC}
${2_CONTORL_SRC}
${3_MODULE_SRC}
${4_DRIVER_SRC}
${5_DRIVER_SRC}
)
作用就是将这四个文件夹下包括子目录中的所有源码添加到源码文件中。
而更底层以及 RTOS 或引入库的部分。引入源码和 include path ,仍然是手动指定。
这样做了以后,所有的垃圾业务源码都可以随便编写随便修改,此前改个命名改一下结构要到处改好多工程配置,现在可以完全流畅地开发业务代码了。业务代码和驱动支持进一步解耦。
在这里有一个前文说到的修改,target_sources
部分现在应该以 PUBLIC 属性定义,否则没办法指定单个文件的编译标志,INTERFACE 属性虽然在实际编译中会进行,也会挂在主构建工程中,但是在 CMAKE 系统中却没有这个文件实例,本质上他是虚挂在源码引入工程 user_src
上,但 user_src
也是一个INTERFACE 接口虚库不会实际编译,所以所有的源码是作为工程 user_src
整体被链接进主构建工程,无法找到任何一个准确的文件了,只是继承主构建工程的编译标志在参与编译。