1 Star 0 Fork 2

jack / utils

forked from 极简美 / utils 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
cmake_template.md 14.36 KB
一键复制 编辑 原始数据 按行查看 历史
极简美 提交于 2023-01-30 17:22 . 更新构建模板

cmake template

一、 组件介绍

组件的目标是代码复用!

  • 对应“功能下移,业务上移”的逻辑,组件应该是对应到具体的一个个功能,不应该包含任何业务逻辑;
  • 从发布形式来看,组件是可以编译成lib库(*.a或者*.so)对外发布的通用模块。

使用cmake组件构建模板的目标,是为了:

  • 统一组件的组织形式,便于查阅与维护;
  • 统一组件的构建方法,便于不通平台的移植;
  • 统一组件的发布方法,便于发布组件化平台;
  • 统一组件的文档总结,便于知识资产的沉淀。

二、组件模板简介

2.1 目录及文件介绍

本仓库是基于cmake构建模块代码的模板,目录与核心文件结构如下:

cmake_template
├── cmake
│   ├── cmake_uninstall.cmake.in
│   ├── commit.cmake
│   ├── config.h.in
│   ├── include.cmake
│   └── option.cmake
├── conf
├── CMakeLists.txt
├── docs
│   └── cmake_template.md
├── example
│   ├── CMakeLists.txt
│   └── example.c
├── LICENSE
├── Makefile
├── README.md
├── source
│   ├── CMakeLists.txt
│   └── source.c
└── unittest
    └── CMakeLists.txt

6 directories, 15 files
  • cmake目录: 用于存放构建相关文件;
    • config.h.in文件: 组件配置文件, 包含版本和宏定义,可用于平台的头文件、函数、库检查,实现跨平台支持;cmake利用此配置文件生成源码使用的*_config.h文件;
    • option.cmake文件: 编译选项,包括版本配置、编译参数、链接参数、宏定义、平台检查选项等,最后的configure_file()命令会根据编译平台把config.h.in生成本模块的*_config.h文件;
    • include.cmake文件: 构建依赖的头文件路径,根据实际情况配置即可,PROJECT_SOURCE_DIR指当前组件的根目录;
    • commit.cmake文件: 提取git仓库当前构建状态的分支名和commit编号,分别保存在GIT_BRANCH_NAMEGIT_COMMIT_HASH变量中,不要修改此文件内容;
    • cmake_uninstall.cmake.in文件: 用于提供模块编译后的头文件、库文件和工具的安装和卸载的方法,不需要对其进行修改。
  • conf目录: 用于存放模块配置文件,比如jsoniniyamlxml文件等;
  • docs目录: 用于存放模块技术总结、设计文档、使用说明文档等,使用markdown编码,便于统一发布htmlpdf文档;
    • cmake_template.md文件: 介绍组件模板库的使用方法,通用的代码组件化指导说明;
  • example目录: 用于存放本模块对外提供的示例程序,便于模块使用者快速上手,模块发布时代源码发布;
    • example.c文件: 示例程序,描述如何使用构建模板生成的*_config.h文件;
    • CMakeLists.txt文件: 示例构建模板,描述如何构建和发布示例程序;
  • source目录: 用于存放本模块的所有源码均;
    • source.c文件: 源码空文件,适配构建模板的使用;
    • CMakeLists.txt文件: 构建库文件模板,只需要添加要构建的源码即可;
  • unittest目录: 用于存放本模块单元测试的所有源码;
    • CMakeLists.txt文件: 单元测试构建模板,待引入单元测试框架后完善;
  • Makefile文件: 对cmake命令的简单封装,简化单一模块的构建验证;
  • CMakeLists.txt文件: 模块构建总入口,非必要不要修改此文件;
  • README.md文件: 使用markdown语法编辑的模块说明文档。
  • LICENSE文件: MIT开源协议,保留版权。

2.2 构建方法

  1. 使用make命令,即可完成Linux平台下组件的编译构建和发布,中间构建文件存放在build目录,对外释放的文件存放在out目录:
[prifix]$ tree -L 1
out
├── bin       # 可执行程序
├── conf      # 配置文件
├── example   # 示例可执行程序(含源码)
├── include   # 对外发布的头文件
├── lib       # 对外发布的库文件
├── lib64     # 对外发布的64库文件
└── tools     # 对外提供的工具程序

若需要对外进行发布,可以编写打包脚本,根据需要打包要用到的文件即可。如此,将构建与打包分离,还可以实现一次构建多次打包/发布,更利于代码的管理与后期维护。

  1. 也可使用cmake命令完成构建,详细步骤如下所示(上面Makefile也只是对下面命令的封装):
mkdir build   # 创建构建目录
cd build
cmake ../ -DCMAKE_INSTALL_PREFIX=$(pwd)/../out  # 指定安装目录
make VERBOSE=1  # VERBOSE会让编译过程可见
make install    # 执行安装
make uninstall  # 执行卸载

其中CMAKE_INSTALL_PREFIX指定了安装目录,还可通过CMAKE_TOOLCHAIN_FILE指定交叉编译工具链,实现组件的移植适配。

三、模板基础使用

3.1 修改项目名称

在顶层CMakeLists.txt文件中,默认使用的当前目录名作为组件的项目名,构建组件库时,库的名称也会使用项目名命名。若组件目录名与项目名不一致时,可修改${component_name}为实际的项目名称:

#Component name, default the same as directory name
get_filename_component(component_dir ${CMAKE_PARENT_LIST_FILE} DIRECTORY)
get_filename_component(component_name ${component_dir} NAME)
project(${component_name})

3.2 修改组件名称

默认情况下,项目名称与组件名称是一样的,都是当前组件的目录名,若组件名与项目名不同(比如libcurl组件,目录名和项目名是libcurl,但其组件名应为curl),则修改顶层CMakeLists.txt文件中如下两行即可:

# set(COMPONENT_NAME template)
set(COMPONENT_NAME ${PROJECT_NAME})

3.3 修改组件版本号

cmake/option.cmake文件前面几行,就是设置组件的版本信息的,可根据组件版本进行配置:

  • COMPONENT_VERSION_MAJOR: 主版本号;
  • COMPONENT_VERSION_MINOR: 次版本号;
  • COMPONENT_VERSION_PATCH: 修订版本号;
  • COMPONENT_VERSION_DRESS: 调试版本号。

3.4 添加头文件路径

若模块比较复杂,头文件都会出现多层目录,编译头文件依赖的路径统一在cmake/include.cmake中添加即可:

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/source)

3.5 组件库构建

组件源码存放在source目录下,添加目录下所有源码的方法:

aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR} ${COMPONENT_NAME}_SRCS)
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/your_source_dir ${COMPONENT_NAME}_SRCS)

其中BUILD_STATIC_LIBSBUILD_SHARED_LIBS选项用于控制是构建动态库还是静态库。

3.6 文件发布与安装

组件一般对外发布库文件、头文件,以及示例程序文件等,可能还包含一些工具文件、配置文件等。要发布一个目标或者文件的方法如下:

# 库文件统一安装到lib目录下
install(TARGETS ${COMPONENT_NAME} DESTINATION lib)
install(TARGETS ${COMPONENT_NAME}_static DESTINATION lib)

# 头文件统一安装到include的${PROJECT_NAME}目录下
install(FILES ${head_file.h} DESTINATION include/${PROJECT_NAME})
install(DIRECTORY ${dir} DESTINATION include/${PROJECT_NAME})

# example统一安装到example的${PROJECT_NAME}目录下
install(TARGETS example DESTINATION example/${PROJECT_NAME})
install(FILES example.c DESTINATION example/${PROJECT_NAME})

3.7 示例程序构建

一个完整的模块,需要有对应的示例程序,便于使用者快速上手和使用。示例程序统一放在example目录中,若模块包含很多子模块,也可以在example目录下再创建子目录。示例程序的构建可参考如下方法:

add_executable(xxx_example xxx_example.c)
target_link_libraries(xxx_example ${COMPONENT_LIBRARY})
install(TARGETS xxx_example DESTINATION example/${PROJECT_NAME})
install(FILES xxx_example.c DESTINATION example/${PROJECT_NAME})

其中COMPONENT_LIBRARY会根据构建的是动态库还是静态库,连接正确的库文件。

3.8 单元测试程序构建

一个完整的模块,同样需要有对应的单元测试程序,以保障模块的高质量的交付。建议所有的单元测试程序全部放在unittest目录中,根据使用的单元测试框架的不同,使用对应的构建方法自动化的进行构建。

四、模板高级使用

此部分介绍核心聚焦在cmake/option.cmakecmake/config.h.in两个文件的配置上面。

4.1 添加编译宏

比如添加OPTION_ENABLE编译选项,对应在cmake/option.cmake中增加如下一行:

option(OPTION_ENABLE "Enable option" ON)

并在cmake/config.h.in中增加如下一行:

#cmakedefine OPTION_ENABLE

执行cmake命令时,就会在组件的config.h文件中生成如下宏定义:

#define OPTION_ENABLE

在程序源码中,就可以包含组件配置文件并使用宏编译选项了:

#include "**_config.h"

#ifdef OPTION_ENABLE
    ......
#else
    ......
#endif

4.2 添加宏定义

添加编译宏与编译选项的方法类似,直接在cmake/option.cmake中使用add_compile_definitions()命令添加宏定义:

add_compile_definitions(OPTION_INT=1234)
add_compile_definitions(OPTION_STR="Hello world!")

源码中即可直接使用如上宏定义了:

#include "**_config.h"

......
printf("option int: %d\n", OPTION_INT);
printf("option str: %s\n", OPTION_STR);
......

为了让程序在各种场景下都能正确的编译,还需要为宏定义添加默认定义,对应在cmake/config.h.in文件中增加:

#ifndef OPTION_INT
#define OPTION_INT      2345
#endif

#ifndef OPTION_STR
#define OPTION_STR      "Hey you!"
#endif

4.3 平台检测

有时候,我们编写的功能依赖于具体平台相关的头文件和函数,当我们在做移植时,首先需要检查目标平台是否具有相关的能力和特性。通过不同平台能力与特性的检测,也有利于编写构建时跨平台的通用组件代码。下面是cmake提供的标准头文件、函数和库检测的方法,并生成对应的编译宏,以供程序源码中使用:

# C头文件检测方法
include(CheckIncludeFiles)
CHECK_INCLUDE_FILES(stdint.h HAVE_STDINT_H)

# C++头文件检测方法
include(CheckIncludeFileCXX)
CHECK_INCLUDE_FILE_CXX(queue HAVE_QUEUE_H)

# 函数检测方法
include(CheckFunctionExists)
CHECK_FUNCTION_EXISTS(poll HAVE_POLL)

# 符号检测方法
include(CheckSymbolExists)
CHECK_SYMBOL_EXISTS(alloca "alloca.h" HAVE_ALLOCA)

# 库检测方法
find_library(HAVE_LIBRT rt)

include(CheckLibraryExists)
CHECK_LIBRARY_EXISTS(rt timer_gettime "" HAVE_LIBRT)

五、其他扩展

5.1 MCU如何存储版本号

<主版本号>.<次版本号>.<修订版本号>.<调试版本号>-<软件构建号>

推荐MCU中使用uint64_t进行保存,主版本号次版本号修订版本号调试版本号分别对应uint8_t类型,取值范围为0~255;构建号对应uint32_t类型,取值为Git代码提交的7位简短Hash值(最高位补零)。

这样不仅解决了MCU存储空间不足的问题,又可以通过版本同步实现线上软件版本,直接定位到对应的软件状态,还可以直接判断该软件是正式版本、测试版本还是其他任何版本。

5.2 调试版本号的特殊用法

调试版本号有时也叫修饰版本号,一般有两种用法。

  1. 调试版本号用于自动化构建

调试版本号根据自动化构建增长,规则是:对外正式发布时,调试版本号始终为0;每次自动构建时,调试版本号占用两个位,奇数代表有代码修改的正式构建,偶数代表没有代码修改的测试构建。这样,每次构建出来对应奇偶两个版本,就可以用于自动化的升级验证。

备注:针对定制化的软件或特殊的软件版本,需要从软件/固件名中进行区分,这样更加直观也更容易维护。

  1. 修饰版本号用于修饰版本的用途

修饰版本号采用十六进制标识,默认情况下为0x00,不同取值代表不同的版本状态:

  • 0x00~0x0F: standard标准版本;
  • 0x10~0x1F: base基础版本;
  • 0x20~0x2F: alpha内测版本;
  • 0x30~0x3F: beta测试版本;
  • 0x40~0x4F: rc正式版本;
  • 0x50~0x5F: release发布版本;
  • 0x60~0x6F: lts长期维护版本;
  • 0x70~0x7F: ultimate旗舰版本;
  • 0x80~0x8F: enhance增强版本;
  • 0x90~0x9F: demo演示版本;
  • 0xA0~0xAF: free自由版本;
  • 0xB0~0xFF: 预留不使用。

备注:使用修饰版本号,可以把定制化的软件或特殊版本也用版本进行区分,但并不直观,且软件升级不容易把控与实现。

实际项目应用时,也可以结合公司的实际场景,将这两种方法做一定的融合。

5.3 交叉编译工具的命名规则

arch[-vendor][-os][-(gnu)eabi]

  • arch: 用于哪个体系架构,如ARMMIPS等。
  • vendor: 工具链提供商,也有以开发板命名的,或者直接是nonecross的。
  • os: 目标操作系统,见过的有Linuxuclinuxbare(无OS)。
  • eabi: 嵌入式应用二进制接口规范(Embedded Application Binary Interface),如gnugnueabi等。其中gnu等价于glibc+oabignueabi等价于glibc+eabi

比如下面的交叉编译工具:

  • arm-none-eabi-gcc: 表示编译ARM架构的裸机系统(包括ARM Linuxbootkernel,不适用编译Linux应用),一般适合ARM7Cortex-MCortex-R内核的芯片使用,一般使用的是newlib专用于嵌入式系统的C库;
  • arm-none-linux-gnueabi-gcc: 表示基于ARM架构的Linux系统,可用于编译ARM架构的u-bootKernel和应用等,使用Glibc库。
C
1
https://gitee.com/jack998/utils.git
git@gitee.com:jack998/utils.git
jack998
utils
utils
master

搜索帮助