开源构建规范

概述

为指导OpenHarmony的开发者开展构建工作,提升构建系统的可重复性、可维护性,提高产品构建质量,构建规范工作组分析总结了各种典型的构建问题,提炼相应的构建规则和建议,制订了本规范。

构建总体原则

P01 构建过程自动化,从构建启动开始到构建最终结束,中间过程不能手工干预。 手工操作容易出错,且浪费时间。将所有的构建操作变成自动化的,从而使构建变得高效、可靠。

P02 构建工程和构建环境代码化。 使用高阶构建框架CMake/Maven/Gradle等描述构建工程;使用Ansible/Dockerfile等描述构建环境。 使用高阶构建框架的目的是向构建人员隐藏构建系统的复杂性。

P03 构建过程可重复、可追溯。 管理构建依赖,始终显式指定固定依赖版本号,确保构建依赖版本一致;将构建环境信息/构建工程作为配置项纳入配置管理,确保构建工程可追溯。

P04 构建脚本简洁清晰,易于维护。 构建脚本也是代码,构建脚本首先是为阅读它的人而编写的,好的脚本应当可以像故事一样发声朗诵出来。

P05 构建标准化 构建目录结构、构建依赖、构建初始化、构建入口、命名等进行标准化约束,使得公司所有产品、平台和组件的构建风格一致,便于构建管理和维护。

构建工程

公共规则

一键式构建

G.COM.01 采用构建脚本,按照交付单元实现一键式自动化构建。

一键式自动化构建是指同一个构建环境下,从构建启动开始到最终结束(最终交付的包生成),中间过程禁止人工干预。 人工干预活动包括但不限于:构建过程中,使用IDE界面进行手工设置、创建或者删除文件目录、创建文件、复制文件、移动文件、删除或者重命名文件、手工设置文件属性、压缩/解压缩文件等。 交付单元是指可独立编译、加载、部署和运行的产品/平台/组件。

【级别】要求

【描述】一键式构建大幅降低构建人员操作复杂度。

【错误示例】某组件的一键式构建只能通过CI系统触发,没有一键式的本地构建。

【错误示例】某组件需要在Xplorer IDE界面手工设置内存映射地址后,再手工编译。

【错误示例】某组件需要手工创建r6c03_view\r6c03_client_view目录。

【正确示例】使用python脚本自动化创建目录:

dir_src = os.getcwd()
dir_client_view = r"r6c03_client_view"
# 处理路径使用os.path可以屏蔽系统差异
dir_mk = os.path.join(dir_src, dir_client_view)

cmd = "{0} {1}".format("mkdir", dir_mk)
cmd_re = subprocess.run(cmd)

构建目录

G.COM.02 构建过程中禁止删除或修改源代码文件及其目录结构。

【级别】禁止

【描述】

  • 构建过程中删除或修改源代码目录结构,会导致构建过程不可重复。

  • 构建过程中,构建输出(包括目标文件、临时文件和构建日志)不能污染源码目录;

  • 构建过程中,避免修改源文件,包括但不限于拷贝、移动、执行dos2unix进行了源代码的格式转换等,源文件的修改应该在构建前的代码准备阶段完成;

  • 工具自动生成源代码应该在构建前的准备阶段完成,如果构建过程中使用工具自动生成源代码,工具自动生成的源代码必须和已有源代码目录隔离,以便区分高价值的源代码和低价值的可重新生成的代码,降低构建系统的复杂性。

【例外】构建补丁时,可能会新增或调整部分源代码。

G.COM.12 构建过程中创建的文件和目录应提供合适的权限。

【级别】要求

【描述】构建过程中可能需要创建目标系统的目录或文件,这些目录和文件应符合权限最小化的设计。 例如,构建过程中应尽量避免在Linux系统中创建“777”权限的目录或文件。

Linux系统中常见的目录文件和权限可以参考《Linux安全配置操作规范》。

构建初始化

G.COM.03 每个组件提供clean命令。
  • 当clean不带任何参数时,清除该层级构建工程下的所有目标文件、临时文件和构建日志,并递归调用下层构建工程的clean,使该构建工程恢复到初始状态;
  • 当clean带参数时,只清除与之对应的构建生成的目标文件、临时文件和构建日志。

【级别】要求

【描述】构建前进行clean是为了避免本次构建受到历史构建残留文件和构建日志的影响,确保构建可重复。必须支持不带任何参数clean;带参数的clean,是为了满足日常交付过程和开发人员本地构建的诉求,不作强制要求。

【正确示例】

base_dir
|---build.suffix
|---logs
|---component_depository_1
|---build.suffix
|---logs
|---component_depository_2
|---build.suffix
|---logs

#不带参数
base_dir/build.suffix clean
#....分别调用component_depository_1和component_depository_2的clean

#带参数:组件名
base_dir/build.suffix clean component_depository_1
#....调用component_depository_1的clean

#带参数 
component_depository_1/build.suffix clean makebin hert umpt
#....调用component_depository_1的umpt单板链接任务的clean,支持详细参数的clean主要应用于内部开发和构建。
G.COM.04 每个组件发布构建,必须保证构建环境中没有历史构建遗留件。

【级别】要求

【描述】首次下载代码,构建环境已经初始化,构建环境本身确保没有历史构建遗留件,可以不用执行clean命令;如果执行过构建的,必须使用clean命令清除历史构建遗留件。

全量构建

G.COM.05 对于版本发布构建,归档的产品全量交付件(含所依赖的所有平台和组件)必须全部重新编译,禁止使用增量编译,禁止使用手工替换文件等方式修改安装盘。

版本发布构建是指产品(含所依赖的所有平台和组件)对外正式发布版本的构建。

【级别】要求

【描述】修改文件后增量编译,会导致部分二进制文件没有更新,造成新的安全编译选项未集成到版本,编译结果不一致。手工替换文件可能会造成构建不可重复、不一致。

构建配置

构建配置数据和构建脚本分离,避免构建工程架构腐化。源码路径、编译选项、目标文件路径等配置与构建脚本放到不同的文件,降低构建脚本维护成本。

G.COM.06 禁止使用与操作系统强绑定的文件(如excel)作为构建配置文件。建议使用跨平台的标准配置文件(如XML)来存放配置选项。

【级别】要求

【描述】使用excel作为配置文件带来的问题:

  • 产品和平台编译过程中,使用excel作为配置文件,都将调用微软的OfficeAPI,每次访问excel表格都会在后台打开excel,处理速度慢。

  • 大量的excel配置需要手动点界面进行操作,可管理性差。

构建日志

G.COM.07 构建输出的日志简洁明晰,构建日志的格式为时间戳+模块名(可选)+日志信息等级+日志内容。

【级别】要求

【描述】建议时间戳格式采取“日期和时间”,如"MM/dd/yyyy HH:mm:ss"。

日志信息等级分为error/warning/informational,级别可以全写,也可以简写;对应的简写为:

级别(大小写都可以) 简写(大小写都可以)
error ERROR
warning WARN
information INFO

建议使用“[]”作分隔符。

【正确示例】 [05/21/2020 00:12:40] [ERROR] mkdir: cannot create directory Permission denied.

【例外】整个日志由工具自动输出的,可用使用以下方式跳过整个日志文件:在日志的最前方(尽可能靠前)输出"This project is built using "+工具名,如"This project is built using CMake."。

G.COM.08 构建日志出现error信息表示构建失败,必须终止构建。

【级别】要求

【描述】出现error信息一般是需要人工干预的构建错误,例如配置的环境变量错误,工具的版本错误,操作系统错误等等;或者软件源代码不对。对于版本发布构建,必须消除构建过程中所有的error消息,不允许屏蔽构建error信息。

【错误示例】某组件构建成功,但构建日志中包含大量的fail、Critical、cannot、not found、missing、no input files等异常信息,令人困惑。

G.COM.09 构建日志文件只保留本次构建的日志,避免本次构建的日志与历史构建的日志混淆。

【级别】要求

【描述】构建日志文件保留历史构建日志会导致混淆错误,比如:最新构建是失败的,由于保留有历史成功构建日志,会误认为最新这次构建是成功的。

G.COM.10 每条日志建议增加对应的模块名,用于问题的快速定界。

【级别】建议

【描述】在日志量较大时,很难快速锁定问题责任模块,需要在日志上加以区分。

【例外】CMake等工具的原生日志,因为输出带有对应模块路径,可以界定问题边界,不用特殊增加模块名维测信息。

构建用户

G.COM.11 禁止使用超级管理员用户root和系统用户执行构建,应该使用普通user账户执行构建。

【级别】要求

【描述】超级管理员用户root和系统用户具有比较高的系统权限,使用此类账户执行构建可能导致构建环境被篡改。

安装态可以使用root用户;执行态使用普通user账户,如果需要使用sudo提升权限的,请遵守《身份和访问管理安全设计规范》。

构建输出文件

G.COM.12 构建输出文件命名后缀遵守业界约定。

【级别】要求

【描述】错误的后缀命名令人误解。

对lib库、obj等构建输出文件的文件缀,应遵从构建工具默认的命名规则。

【错误示例】某文本文件命名为XXX.lib。

【错误示例】某object文件命名为XXX.a。

【错误示例】某静态库命名无后缀,命名为libxxx。

【正确示例】业界如下网址可以查询常见的文件后缀命名约定:http://www.fileextension.org/ , https://fileinfo.com/ , https://www.file-extensions.org/, http://file-extension.net/ 。

下面是一些常见的文件后缀的命名约定:

文件后缀名 类型约定 文件后缀名 类型约定
.a 静态库 .so 动态库
.o object文件 .7z 7zip压缩文件
.tar tar存档文件 .gz/.gzip GNU压缩存档文件
.pack java pack200压缩文件 .rar/.rar5 rar压缩包

C/C++构建工程

构建目录

G.C&C++.01 构建目录结构标准化。

构建目录按用途分为源树Source Tree、构建中间件树Build Tree、构建安装树Install Tree三种。

  • Source Tree是保存源码和构建脚本的目录。
  • Build Tree是保存构建中间件的目录,目录名称一般为"build"。
  • Install Tree是保存构建发布件的目录,目录名称固定为"output"。

Source Tree、Build Tree和Install Tree目录隔离,互相不重叠,没有交集,即不允许一个目录同时承担两种及以上的用途,譬如一个目录既作为Source Tree存放源码,又作为Build Tree存放编译中间件,这是不允许的。

Source Tree包含下列文件和目录:

  • 构建工具入口文件,如CMakeLists.txt,CMakeLists.txt中通过add_subdirectory()命令添加子目录,CMake将自动迭代调用子目录中的CMakeLists.txt,并逐级向下展开。
  • build.suffix脚本文件,该文件是一键式构建入口,仅调用该脚本即可完成构建。".suffix"表示对应的构建脚本语言后缀,譬如".bat",".sh",".py"等。
  • config.suffix配置文件,该文件用于存放构建配置项,是唯一的配置文件入口。
  • 构建脚本目录,可选,如cmake目录,用于保存CMake脚本文件。CMake脚本文件包括宏、函数、toolchain等, CMakeLists.txt通过include()命令包含CMake脚本文件,并调用其中的宏、函数等。
  • 组件代码目录,用于存放各组件的源码及构建脚本。
  • 上述文件和目录,只有CMakeLists.txt、build.suffix、config.suffix这三个文件是必需的,其它文件或者目录仅用作示例,不强制要求。

Build Tree包含下列目录:

  • build目录,用于存放构建中间件。该目录可能在构建过程中创建,在git库上可能没有该目录。
  • 有的工程已经将build目录用于保存构建脚本,可以创建别的目录作为Build Tree。

Install Tree包含下列目录:

  • output目录,用于存放交付件。该目录可能在构建过程中创建,在git库上可能没有该目录。

【级别】要求

【描述】

典型目录结构如下:

base_dir
|---CMakeLists.txt      ---|
|---build.suffix           |
|---config.suffix          |
|---cmake                  |--> Source Tree
|---component_1            |
|---component_2            |
|---......                 |
|---component_n         ---|
|---build               ------> Build Tree
|---output              ------> Install Tree

各组件的目录结构与顶层的目录结构类似,譬如:

component_1
|---CMakeLists.txt      ---|
|---build.suffix           |
|---config.suffix          |
|---cmake                  |--> Source Tree
|---module_1               |
|---module_2               |
|---......                 |
|---module_n            ---|
|---build               ------> Build Tree
|---output              ------> Install Tree
G.C&C++.02 构建过程中禁止以任何形式修改Source Tree。

【级别】建议

【描述】构建过程中修改Source Tree会导致构建过程不可重复。

常见的修改Source Tree的操作有: 1)打补丁 2)打点 3)裁剪 4)自动生成源码 5)先修改源码然后还原 6)增加/修改/删除临时文件或者目录 7)修改文件/目录属性或者格式,譬如修改文件可执行权限、dos2unix等

建议解决方案如下: 1)将代码拷贝到Build Tree,然后打补丁,编译。 2)打点工具修改源码,使得构建过程不可信,因此禁止在构建过程中使用打点工具。应将打点后的代码上传到代码库,使用打点后的代码进行构建。 3)裁剪是独立的源码交付需求,裁剪可以看做是代码准备阶段。裁剪前的版本和裁剪后的版本都必须满足在构建过程中不修改Source Tree。 4)自动生成的源码应放在Build Tree下。 5)先修改源码然后还原是掩耳盗铃,构建过程中源码已经发生了变更。 6)临时文件或者目录都应该放在Build Tree下。 7)必须保证代码库中的文件属性和格式是正确的,而不是构建时修改。

检验Source Tree是否发生变化的方法之一:编译完成后在源码目录下执行git status命令,不能有任何变更。先修改后还原导致的Source Tree变更,通过git status可能检测不出来。

【例外】 1)git status检测到Build Tree和Install Tree这两个目录的变更是允许的。 2)git status检测到由于裁剪导致的变更是允许的。

G.C&C++.03 Windows构建根目录建议为D:\交付单元的名称+版本号(可选);Linux构建根目录建议为/usr1/交付单元的名称+版本号(可选)。

【级别】建议

【描述】构建根目录按交付单元的名称+版本号命名,禁止使用build或code等无法区分交付单元的目录名称。 清晰的构建目录结构,便于测试人员配置构建参数、执行一键式构建入口和对比构建结果。 根目录示例如下:

D:\Offering [Version,可选]或/usr1/Offering [Version,可选]
G.C&C++.04 构建过程中生成的所有中间件保存在Build Tree中。

【级别】要求

【描述】构建过程中产生的中间件包括构建工具CMake自动生成的makefile、构建脚本自动生成的源码、构建脚本拷贝的源码及补丁、编译产生的object文件、库文件、可执行程序、构建日志等等。如果中间件放在Build Tree以外的目录,势必污染Source Tree或者Install Tree。因此,所有中间件都要保存在Build Tree中。Build Tree仅用于保存构建中间件,不能将Source Tree下某个放置源码或者构建脚本的目录用作Build Tree。 Build Tree下创建构建日志子目录logs,构建日志后缀文件命名为.log。

G.C&C++.05 支持指定Source Tree和Install Tree以外的任意目录作为Build Tree。

【级别】要求

【描述】支持指定Source Tree和Install Tree以外的任意目录作为Build Tree,做到构建过程与目录无关。在哪个目录下执行构建,哪个目录就是Build Tree,编译中间件就保存在哪个目录下。Build Tree的目录名称一般为“build”,也可以使用其它名称。

【正确示例】使用CMake系统变量CMAKE_BINARY_DIR和CMAKE_CURRENT_BINARY_DIR访问Build Tree,避免Build Tree与Source Tree产生耦合。

G.C&C++.06 所有发布件保存在Install Tree中。

【级别】要求

【描述】本地编译场景下,发布件直接"install"到HOST Computer上并运行。交叉编译场景下,发布件并不在HOST Computer上运行,而是在TARGET Computer上运行。

发布件包括库文件、可执行程序、包文件、头文件等,是组件对外的二进制接口。所有发布件都保存在Install Tree中,不应将发布件放在Install Tree以外的目录下。

Install Tree只用于保存发布件,不应将编译中间件放在Install Tree中。

G.C&C++.07 支持指定Source Tree和Build Tree以外的任意目录作为Install Tree。

【级别】要求

【描述】支持指定Source Tree和Build Tree以外的任意目录作为Install Tree,做到构建过程与目录无关。Install Tree的目录名称固定为“output”。

【正确示例】CMake构建工程应支持通过系统变量CMAKE_INSTALL_PREFIX指定Install Tree的根目录。

构建入口

G.C&C++.08 每个交付单元的构建入口单一。构建脚本入口名称统一命名为build.suffix,并且路径要求在构建根目录下。

【级别】要求

【描述】通过使用一致的构建入口点,构建过程可以变得更加高效和可自动执行。每个交付单元只有单一构建入口,便于一键式自动构建。

【错误示例】如下构建有多个入口点,如果没有说明文档,无法确认哪一个入口是正确的,造成选择困难。 build.bat build_all.sh build_v6.sh

【正确示例】一键式构建脚本build.sh的典型写法如下:

#!/bin/bash

if [ -d "build" ]; then
    rm -fr build/*
else
    mkdir build
fi

if [ -d "output" ]; then
    rm -fr output/*
else
    mkdir output
fi

cd build
cmake ..

cpu_processor_num=$(grep processor /proc/cpuinfo | wc -l)
job_num=$(expr "$cpu_processor_num" \* 2)
echo Parallel job num is "$job_num"
make -j"$job_num"
G.C&C++.09 支持指定target进行构建。

【级别】要求

【描述】日常开发场景下,通过指定target编译,开发人员只需要编译修改了的代码,不需要编译全部代码,达到快速验证的目的。编译工程应支持指定target进行构建,从而满足灵活多变的编译调试需求。

【正确示例】典型命令如下:

base_dir # cd build
base_dir/build # cmake ..
# 编译全部目标
base_dir/build # make
# 编译特定目标
base_dir/build # make target_name
G.C&C++.10 支持重复编译。

【级别】要求

【描述】编译成功后,不对源代码做任何修改,不清理上次编译的中间件和发布件,不修改编译环境,再次执行编译,必须能重复编译成功。

G.C&C++.11 支持增量编译。

【级别】建议

【描述】日常开发场景下,增量编译可以缩短编译时间,提高开发效率,因此建议支持增量编译。

G.C&C++.12 支持并行编译。

【级别】要求

【描述】通过"make -jN"命令进行并行编译,可以提高编译速度。本规则仅适用于使用make工具的工程。

支持jobserver统一调度,使整个工程的负载最优。不能出现下面两个告警:

warning: jobserver unavailable: using -j1.  Add '+' to parent make rule.
warning: -jN forced in submake: disabling jobserver mode.

支持jobserver的方法如下:

  1. 通过$(MAKE)直接调用make命令

    ExternalProject_Add(foo
        SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
        CONFIGURE_COMMAND sh configure_ext.sh
        BUILD_COMMAND $(MAKE)
    )
    
  2. 通过shell脚本调用make命令

    ExternalProject_Add(foo
        SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
        CONFIGURE_COMMAND sh configure_ext.sh
        BUILD_COMMAND sh build_ext.sh $(MAKE)
    )
    

    build_ext.sh内容如下:

    #!/bin/bash
    
    make
    

    注意:build_ext.sh不需要解析和使用参数$(MAKE)。

  3. 通过python脚本调用make命令

    ExternalProject_Add(foo
        SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
        CONFIGURE_COMMAND sh configure_ext.sh
        BUILD_COMMAND python build_ext.py $(MAKE)
    )
    

    build_ext.py内容如下:

    #!/usr/bin/python
    # -*- coding: UTF-8 -*-
    
    import subprocess
    
    def main():
        child = subprocess.Popen("make", close_fds=False)
        ret = child.wait()
        return
            
    if __name__ == '__main__':
        main()
    

    注意:build_ext.py不需要解析和使用参数$(MAKE)。

构建依赖

G.C&C++.13 定义一个构建依赖文件dependence.xml,文件中描述构建依赖的所有组件。构建脚本自动读取该依赖文件,用于制作最终的软件包。

【级别】建议

【描述】按照依赖文件进行软件包制作,避免在构建脚本中定义依赖组件,提高构建过程可维护性。

构建配置

G.C&C++.14 构建根目录的config.suffix配置文件是整个交付项目唯一的配置入口。

【级别】要求

【描述】顶层的config.suffix中,应暴露最少的配置项,只需要用户配置的构建环境、构建工具相关的信息。

【例外】如果构建配置的内容非常少,采取系统键值对配置项,配置文件可以命名成config.conf。

GN 编写规范

编译规范

规则1.1 禁止在gn中调用外部编译工具编译软件模块

【级别】禁止

【描述】需要将外部组件移植成gn的编译形式,避免编译过程对环境产生不必要的依赖,而且可获得编译框架提供的公共能力,包括不限于:安全编译选项,ASAN等。

【反例】在gn中使用action调用automake和Make来编译三方组件。

【例外】Linux Kernel 编译框架实际完成的用户态程序编译,内核完全可以在编译框架之外完成独立编译。某些平台实现为了实现一键编译,使用gn将内核编译加在编译过程中,是可以接受的。

规则1.2 禁止在模块的gn文件中,再次添加编译系统已经添加的安全编译选项

【级别】禁止

【描述】对于全局已经添加的默认选项,模块开发者应当知晓,不需要为了满足内外部规则再次添加。

编译选项 编译参数 默认值
栈保护 -fstack-protector-strong
Fortify Source -D_FORTIFY_SOURCE=2 -O2

【反例】在模块的编译添加 -fstack-protector-strong

规则1.3 禁止在gn中添加和默认编译选项相反的编译选项

【级别】禁止

【描述】默认的编译选项代表了系统的默认能力,自研模块有特殊情况需要去掉部分能力,必须有足有的理由。

【反例】在自研模块中添加 -wno-unused 以消除编译告警。

【例外】移植三方组件,或者使用因为三方组件时,可根据三方组件的要求覆盖默认的编译选项。

规则 2.1 使用gn format 对添加或者修改的gn文件进行格式化,满足格式和排版的需求

【级别】要求

规则 2.2 编写action时,使用python而不是shell

【级别】建议

【描述】python 环境更容易保持统一,可以比较容易的多重操作系统上运行,并且扩展性可读性可测试更好。

规则 2.3 禁止在gn和ninja执行过程修改源码目录的内容

【级别】禁止

【描述】包括但不限于给源码目录打patch,向源码目录中拷贝,在源码目录中执行编译,在源码目录生成中间文件等。

规则 2.4 编译脚本的编码格式设置为utf-8,换行符设置为unix格式

【级别】要求

【反例】在windows上编写脚本后,使用了中文注释并保存为本地编码。