# 开源构建规范

## 概述

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

## 构建总体原则

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

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

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

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

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

## 构建工程

### 公共规则

#### 一键式构建

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

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

【级别】要求

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

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

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

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

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

```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的典型写法如下：

```bash
#!/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命令

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

2. 通过shell脚本调用make命令

   ```cmake
    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内容如下：

    ```bash
    #!/bin/bash

    make
    ```

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

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

   ```cmake
    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内容如下：

    ```bash
    #!/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上编写脚本后，使用了中文注释并保存为本地编码。