前言
在看openharmony源码时发现和其他开源linux的比较大的区别是实现了多平台的支持和一些系统组件,而且是统一构建的,在做一些更改时也只能参考一些已有的文章,但稍微有点特殊的需求,便有点心虚不知如何下手了,尤其是编译构建的相关的问题,所以有了这篇源码剖析记录。
概述
编译构建框架以gn+ninja作为基础构建系统,(不熟悉gn和ninja的可以参考这篇文章,大概了解下gn快速入门笔记),针对产品需要和部件化功能,在gn阶段前增加了preloader和loader的预加载过程。
- preloader: 产品信息预加载。根据编译产品名称,加载对应的配置文件(config.json)并解析,将解析结果以文本方式输出到指定目录下。
- loader:子系统、部件和模块信息加载。以编译产品名称和preloader的输出作为输入,加载对应的subsystem、component、module/part等配置信息,以文本文件方式输出结果文件和gn的编译任务。
- gn: gn是一种编译构建工具,用于产生编译目标之间的图状编译依赖。在编译流程中,编译文件生成阶段负责构建参数赋值,执行gn gen编译命令,产生的build.ninja文件可以类比与makefile文件。
- ninja:ninja是一种接近汇编级的编译构建工具。语法规则简单,构建速度快。在编译流程中,根据编译规则完成对所有编译目标的编译过程。
build.sh脚本编译
OpenHarmony有两种编译方式,一种是通过hb工具编译,一种是通过build.sh脚本编译,而通过看代码我们会发现实际build.sh的方式最终也会调用的hb工具 ,所以说两种方式不是相互独立的,而是build.sh是hb的上层。
- build.sh的使用
在这里想给出的一个说明是build.sh脚本可通过-h命令去查看使用方法,具体见附录一
由于系统编译过程比较慢,大家可以多多参考帮助信息,查看是否有适合自己的编译选项,例如可以使用如下命令只编译系统的组件hellonapi,
shell
./build.sh --product-name rk3568 --ccache --build-target=hellonapi
在本地没有修改gn和产品配置相关文件的前提下,添加--fast-rebuild会让你直接从ninja编译,可以加快编译过程,等等
- build.sh源码梳理
在进行build.sh编译前首先需要执行
bash build/prebuilts_download.sh
命令安装编译相关的环境,可以通过配置指定数据源,默认从华为提供的源中下载数据,并配置相关的环境。只支持linux和mac平台以及arm64和x86_64架构的CPU。
以下为build.sh脚本执行的大体流程,由于代码比较简单,就不再详细说明了
-
检测shell环境:支持linux下的支持,并将shell更改为dash
-
查询系统环境,并根据系统配置相关的一些环境变量(python、node、ohpm)
npm config set registry https://repo.huaweicloud.com/repository/npm/
npm(Node Package Manager)是Node.js的默认包管理器,用于发布、安装和管理JavaScript包(modules)
-
检测编译依赖的工具,不同的系统有不同的依赖,配置在文件(build/scripts/build_package_list.json)。
-
调用主编译程序(build/hb/main.py)
/build/hb/main.py build $args_list
- $args_list:输入的命令列表
到此处可发现后续就是采用的hb工具来实现的。
hb工具源码概览
hb是用python写的编译脚本,主函数目录如下:hb\main.py,以下为代码的主函数,主要执行过程可分为初始化和执行两部分。
python
module_initializers = { #任务列表
'build': main._init_indep_build_module if main._is_indep_build() else main._init_build_module,
'init': main._init_hb_init_module,
'indep_build': main._init_indep_build_module,
'set': main._init_set_module,
'env': main._init_env_module,
'clean': main._init_clean_module,
'tool': main._init_tool_module,
'install': main._init_install_module,
'package': main._init_package_module,
'publish': main._init_publish_module,
'update': main._init_update_module,
'push': main._push_module
}
module_type = sys.argv[1] #获取设置的功能类型,例如build、set等
......
module = module_initializers[module_type]() #注:这是一个函数直接执行,也就是初始化过程
try:
module.run() #执行过程
if module_type == 'build':
LogUtil.hb_info('Cost Time: {}'.format(SystemUtil.get_current_time() - start_time))
except KeyboardInterrupt:
.......
return -1
else:
return 0
在带参数调用hb时实际会执行 module = module_initializers[module_type]()
这段代码从而实现对功能类别的筛选,做对应的初始化 ,例如执行hb build xxx时便会调用三目运算函数main._init_indep_build_module if main._is_indep_build() else main._init_build_module
。本文的目的是做个概览,build这个参数和其他的参数的执行过程雷同,所以后面只说明build的这个参数的过程。从设计模式的角度看它就是个抽象工厂的模式。
类图关系如下,详细分析的码友建议对着这个图片看:
如果有码友想自己生成的话可以参考根据python代码自动生成类图的实现方法[附带python源码],当然通过这个博客还不能完全做成这样,还需要简单的修改一下。
build初始化过程
可通过如下命令单独执行脚本:
shell
/build/hb
$ python main.py build --product-name purple_pi_oh --ccache
在build时可见首先执行main._init_indep_build_module if main._is_indep_build() else main._init_build_module
,当_is_indep_build
返回true时执行_init_indep_build_module
,否则执行_init_build_module
。
--indep-build
是一个用于独立构建的命令行选项。这个选项允许开发者单独编译和构建特定的组件或模块,而不是整个系统,详细信息可见indep_build选项说明。
由上图可知正常的流程是通过_init_build_module函数执行编译流程。
python
def _init_build_module(self) -> BuildModuleInterface:
args_dict = Arg.parse_all_args(ModuleType.BUILD)#1.解析构建的配置文件
if args_dict.get("product_name").arg_value != '':# 如果配置了厂家名称
set_args_dict = Arg.parse_all_args(ModuleType.SET) #解析配置文件setargs.json
set_args_resolver = SetArgsResolver(set_args_dict) # 2.设置数据项的处理函数
ohos_set_module = OHOSSetModule(set_args_dict, set_args_resolver, "")
ohos_set_module.set_product() #3.根据设置的厂家信息做匹配
preloader = OHOSPreloader() # 4.产品信息预加载
loader = OHOSLoader() # 5.子系统、部件和模块信息加载
generate_ninja = Gn() # 6.编译构建工具,用于产生编译目标之间的图状编译依赖
ninja = Ninja() #7.汇编级的编译构建工具
build_args_resolver = BuildArgsResolver(args_dict)#8.设置数据项的处理函数,类似第2步
# 所有的以上操作其实也只是为了生成对应的实例,提供给OHOSBuildModule进行初始化
return OHOSBuildModule(args_dict, build_args_resolver, preloader, loader, generate_ninja, ninja)
1. 解析构建的配置文件
通过parse_all_args 函数利用json库 解析文件buildargs.json,并将解析的文件配置成argparse库的形式,并对数据类型及配置的数据做校验,便于后续配置应用。通过查看其他类型例如SET、ENV等过程也有类似的配置文件,配置方式也类似,如下:

下面选取了buildargs.json文件的一项,通过此项大概说明每个字段的含义。
json
{
"target_cpu": {
"arg_name": "--target-cpu", //配置选项名称
"argDefault": "arm", //配置项默认值
"arg_help": "Default:''. Help:Specifies the desired cpu architecture for the build, each may support different cpu architectures, run 'hb set --all' to list product all supported cpu architectures", //帮助信息,实际的-h获取到的帮助信息也是直接从此文件中获取的
"arg_phase": "prebuild",//包含prebuild、preload、load、preTargetGenerate、targetGenerate、postTargetGenerate、preTargetCompilation、targetCompilation、postTargetCompilation、postbuild,当前的编译参数通过统一的文本文件管理,保证参数功能独立,并一一映射对应的参数实现函数。在编译过程中,为了明确参数具体的作用时间,根据编译的preloader,loader,gn,ninja四个基础过程,人为划分了9个编译阶段
"arg_type": "str", //包含bool、str、list、subparsers,决定了传入参数的类型
"arg_attribute": {
"optional": [ //选其中之一
"arm",
"arm64",
.....//省略
]
},
"resolve_function": "resolve_target_cpu",//解决此选项的函数名称
"testFunction": "testBuildTargetCpu" //测试此选项的函数名称
},
.......
此例子中有一项未提及,即"abbreviation"字段,此字段为简写说明。
arg_phase字段每个选项的含义如下:
shell
PRE_BUILD # 编译预处理相关参数,如--full-compilation,--log-level
PRE_LOAD # preloader前置设置参数,
LOAD # loader前置设置参数,如--scalable-build,--build-xts
PRE_TARGET_GENERATE # gn前置设置参数,如--gn-args
TARGET_GENERATE # gn阶段参数
POST_TARGET_GENERATE # gn后置设置参数
PRE_TARGET_COMPILATION # ninja前置设置参数,如--ninja-arg
TARGET_COMPILATION # ninja阶段参数
POST_TARGET_COMPILATION # ninja后置设置参数,如--generate-ninja-trace
POST_BUILD # 编译后处理参数,如--clean-args
2.设置数据项的处理函数
将配置文件中的resolve_function字段对应的值设置为处理函数。
代码流程
- hb\main.py
python
set_args_resolver = SetArgsResolver(set_args_dict)
|-->SetArgsResolver:def __init__(self, args_dict: dict) #SetArgsResolver类的构造函数
|-->super().__init__(args_dict) # 调用基类ArgsResolverInterface的构造函数
|-->self._args_to_function = dict() #创建函数字典
|-->self._map_args_to_function(args_dict) #设置回调函数名称到set_args_resolver中
ohos_set_module = OHOSSetModule(set_args_dict, set_args_resolver, "") #将获取的参数设置到ohos_set_module实例中
ohos_set_module.set_product() #
|-->self.args_resolver.resolve_arg(self.args_dict['product_name'], self)
|-->resolve_function = self._args_to_function[target_arg.arg_name]
|-->return resolve_function(target_arg, module) #调用回调函数
处理函数(让子类能够映射参数到解析函数上)
python
def _map_args_to_function(self, args_dict: dict):
for entity in args_dict.values(): #遍历字典
if isinstance(entity, Arg) and entity.arg_name != 'sshkey':#检查每个值是否是 Arg 类型且参数名不是 'sshkey'
args_name = entity.arg_name
function_name = entity.resolve_function
if not hasattr(self, function_name) or \
not hasattr(self.__getattribute__(function_name), '__call__'):#来获取方法,并检查它是否是可调用的,
raise OHOSException(
'There is no resolution function for arg: {}'.format(
args_name),
"0004")
entity.resolve_function = self.__getattribute__(function_name)#解析函数赋值给 entity.resolve_function
self._args_to_function[args_name] = self.__getattribute__(
function_name)#解析函数存储在 _args_to_function 字典中
此处有个疑问__getattribute__
如何获取到的解析函数呢?这个就是多态,基类访问派生类,有兴趣的可以使用dir()函数查看基类中都包含哪些属性,在本示例中是包含resolve_product_name和resolve_set_parameter这两个派生类(SetArgsResolver)中的静态成员函数的。为了更好的说明这点我写了demo,大家有兴趣的可以看这个文章openharmony编译过程(python)中如何实现在配置文件中配置相关的实现函数的?
3. 根据设置的厂家信息做匹配
主要关注的函数为hb\resolver\set_args_resolver.py:resolve_product_name.
python
def resolve_product_name(target_arg: Arg, set_module: SetModuleInterface):
|-->config = Config() #获取配置文件信息,并将resources/config/config.json文件拷贝到out/ohos_config.json为了和下文的config区分,后续写的都会用ohos_config.json表示此处的配置文件
|--> .......
|-->get_product_info(product_name: str, company=None)
|-->ProductUtil.get_products()
|--> config_path = os.path.join(p_config_path, 'config.json') #获取配置文件\vendor\厂家名\产品名\config.json
涉及配置文件及对应解释如下:
- config.json
json
{
"product_name": "purple_pi_oh", #产品名称
"device_company": "rockchip", #厂家名称
"device_build_path": "device/board/industio/purple_pi_oh", #设备子系统目录,preloader阶段从该目录下加载对应子系统部件配置信息。
"target_cpu": "arm",#表示目标平台的CPU类型
"type": "standard",#包含mini、small、standard
"version": "3.0",
"board": "purple_pi_oh",
"api_version": 8,
"enable_ramdisk": true,
"enable_absystem": false,
"build_selinux": true,
"build_seccomp": true,
"inherit": [ "productdefine/common/inherit/rich.json", "productdefine/common/inherit/chipset_common.json" ],
"subsystems": [
{
"subsystem": "security",
"components": [
{
"component": "selinux_adapter",
"features": []
}
]
},
......
]
}
- ohos_config文件信息
生成在out目录下的ohos_config文件内容如下,默认文件(resources/config/config.json)中都是默认空值。
json
{
"root_path": "/home/zcc/3566",
"board": "purple_pi_oh",
"kernel": null,
"product": "purple_pi_oh",
"product_path": "/home/zcc/3566/vendor/industio/purple_pi_oh",
"device_path": "/home/zcc/3566/device/board/rockchip/purple_pi_oh",
"device_company": "rockchip",
"os_level": "standard",
"version": "3.0",
"patch_cache": null,
"product_json": "/home/zcc/3566/vendor/industio/purple_pi_oh/config.json",
"component_type": "",
"product_config_path": "/home/zcc/3566/vendor/industio/purple_pi_oh",
"target_cpu": "arm",
"target_os": null,
"out_path": "/home/zcc/3566/out/purple_pi_oh",
"subsystem_config_json": "build/subsystem_config.json",
"device_config_path": "/home/zcc/3566/device/board/rockchip/purple_pi_oh",
"support_cpu": null,
"precise_branch": null,
"compile_config": null,
"log_mode": "normal"
}
解析配置文件有多个目录,略有不同,具体的区别或者说这样做的目的,还不是很清楚,需要详细了解的可参照函数(hb\util\product_util.py:def get_products()) 根据我实测发现我这个配置执行的代码如下:
python
if config.vendor_path != '':#默认目录为代码根目录的vendor文件夹
for company in os.listdir(config.vendor_path):#遍历每个厂家的目录,可避免必须配置厂家信息
company_path = os.path.join(config.vendor_path, company)
if not os.path.isdir(company_path):
continue
for product in os.listdir(company_path):#遍历厂家的每个产品列表
product_path = os.path.join(company_path, product)
config_path = os.path.join(product_path, 'config.json')
if os.path.isfile(config_path):
info = IoUtil.read_json_file(config_path)
product_name = info.get('product_name')
if product_name is not None:
yield {
'company': company,
"name": product_name,
'product_config_path': product_path,
'product_path': product_path,
'version': info.get('version', '3.0'),
'os_level': info.get('type', "mini"),
'config': config_path,
'component_type': info.get('component_type', '')
}
4.产品信息预加载
代码梳理:
python
preloader = OHOSPreloader()
|-->OHOSPreloader:def __init__(self):
|-->PreloadInterface:def __init__()
|--> self._config = Config()# 获取配置文件信息
|-->OHOSPreloader:self._toolchain_label = ""
|-->.......
通过以上代码可发现此处主要是对OHOSPreloader类默认参数的初始化,并将配置信息赋值给成员变量_config。
5.子系统、部件和模块信息加载
代码梳理
python
class OHOSLoader(LoadInterface):
|-->super().__init__()
|--> LoadInterface:self._config = Config()# 获取配置文件信息
|-->self.source_root_dir = "" #初始化默认参数
|-->.......
通过以上代码可发现此处主要是对OHOSPreloader类默认参数的初始化,并将配置信息赋值给成员变量_config。
6.编译构建工具,用于产生编译目标之间的图状编译依赖
配置Gn执行文件
python
class Gn(BuildFileGeneratorInterface):
|-->......
|-->self.config = Config()# 获取配置文件信息
|-->self._regist_gn_path()# 设置gn工具的目录
7.汇编级的编译构建工具
配置ninja执行文件
python
class Ninja(BuildExecutorInterface):
def __init__(self):
super().__init__()#创建实例对象
self.config = Config()#获取配置文件
self._regist_ninja_path()#配置ninja的执行文件目录
build执行过程
执行过程主要是根据初始化部分解析的配置文件(buildargs.json),然后根据配置文件中配置的不同阶段(PRE_BUILD、PRE_LOAD等)进行执行的。
主要的执行过程可参考类BuildModuleInterface
的run函数,
python
def run(self):
try:
self._prebuild_and_preload()
self._load()
self._gn()
self._ninja()
except OHOSException as exception:
raise exception
else:
self._post_target_compilation()
finally:
self._post_build()
它会对初始化过程中的回调函数进行回调例如_load函数会回调到子类(OHOSBuildModule)的_load函数中。
python
def _load(self):
self._run_phase(BuildPhase.LOAD)#执行初始化过程中的回调函数
if self.args_dict.get('fast_rebuild', None) and not self.args_dict.get('fast_rebuild').arg_value:
self.loader.run()
第二行的代码会回调初始化过程中配置文件(buildargs.json)中的函数。对于这部分回调就和初始化的"2.设置数据项的处理函数"对应上了。
执行过程如下:
python
def _run_phase(self, phase: BuildPhase):
'''Description: Traverse all registered parameters in build process and
execute the resolver function of the corresponding phase
@parameter: [phase]: Build phase corresponding to parameter
@return :none
'''
for phase_arg in [arg for arg in self.args_dict.values()if arg.arg_phase == phase]:
self.args_resolver.resolve_arg(phase_arg, self)
执行过程流程图如下:
附件一编译命令支持选项
shell
-h, --help show this help message and exit
--target-cpu {arm,arm64,x86_64,x64,mipsel,riscv64,loongarch64}
Default:''. Help:Specifies the desired cpu architecture for the build, each may support different cpu architectures, run 'hb set --all' to list product all supported cpu architectures
--target-os {android,ios}
Default:''. Help:Specifies the desired os type for the build, each may support different os type, run 'hb set --all' to list product all supported os type
-p PRODUCT_NAME, --product-name PRODUCT_NAME
Default:''. Help:Build a specified product. You could use this option like this: 1.'hb build --product-name rk3568@hihope' 2.'hb build --product-name rk3568'
--rename-last-log [RENAME_LAST_LOG]
Default:True. Help:You can use it to decide whether to keep the last build log
--log-mode {normal,silent}
Default:'normal'. Help:You can use this option to determine whether to use single-line refresh log mode
--precise-branch PRECISE_BRANCH
Default:'dayu200_tdd'. Help:You can use this option to select the dayu200_tdd branch
--ccache [CCACHE]
Default:True. Help:Enable ccache, this option could improve compilation speed. --stat-ccache can summary the cache data
--xcache [XCACHE]
Default:False. Help:Enable xcache, this option could improve compilation speed. --stat- ccache can summary the cache data
--enable-pycache [ENABLE_PYCACHE]
Default:False. Help:Enable pycache, This option can improve the execution speed of pytho files
--jobs JOBS
Deprecated, please do not use this option
--disable-part-of-post-build [DISABLE_PART_OF_POST_BUILD ...]
Deprecated, please do not use this option
-T [BUILD_TARGET ...], --build-target [BUILD_TARGET ...]
Default:[]. Help:You use this option to specify a single compilation target, and use 'hb tool --ls' to list all build target
--ninja-args [NINJA_ARGS ...]
Default:[]. Help:You can use it to pass parameters for the ninja phase, but you need to follow the specified command format. eg. --ninja-args=-dkeeprsp
-f [FULL_COMPILATION], --full-compilation [FULL_COMPILATION]
Default:[]. Help:You can use it to start full code compilation. The default compilation target is images. Use this option to add 'make_all' and 'make_test' to the build process.
--strict-mode [STRICT_MODE]
Default:False. Help:Check all produce of each phase to early terminates a potentially
problematic compilation.
--scalable-build [SCALABLE_BUILD]
Default:False. Help:Select whether to read information from parts.json generate by preload
--build-example [BUILD_EXAMPLE]
Default:False. Help:Select whether to read information from subsystem_config_example.json
--build-platform-name BUILD_PLATFORM_NAME
Default:'phone'. Help:Name of the compilation platform. The current optional value is
'phone'
--build-xts [BUILD_XTS]
Default:False. Help:Select whether to load the components included in the subsystem xts
--ignore-api-check [IGNORE_API_CHECK ...]
Default:[]. Help:Skip the check of some subsystems
--load-test-config [LOAD_TEST_CONFIG]
Default:True. Help:Select whether to load the test field in bundle.json, that is, whether to call the test case
--skip-partlist-check [SKIP_PARTLIST_CHECK]
Default:False. Help:Skip the subsystem and component check in partlist file
--build-type {release,profile,debug}
Default:'release'. Help:Specify compile release or debug version
--log-level {info,debug}
Default:'INFO'. Help:Specify the log level during compilation. you can select two levels:debug, info. In debug mode, it show all command lines while building, including cxx, link, solink, etc.
--export-para [EXPORT_PARA ...]
Deprecated, please do not use this option
--test [TEST ...]
Default:[]. Help:You can use it to choose test type. eg. --test xts
--gn-args [GN_ARGS ...]
Default:[]. Help:Specify gn build arguments, you could use this option like this 'hb build --gn-args is_debug=true'
--gn-flags [GN_FLAGS ...]
Default:[]. Help:Specify gn build arguments, you could use this option like this 'hb build --gn-flags "--export-compile-commands"
-c COMPILER, --compiler COMPILER
Deprecated, please do not use this option
--fast-rebuild [FAST_REBUILD]
Default:False. Help:You can use it to skip prepare, preloader, gn_gen steps so we can enable it only when there is no change for gn related script
--root-perf-main {root,main,root_main}
Default:root. Help:different kinds of root packages
--runtime-mode {release,debug,profile}
Default:release. Help:runtime mode
--check-compilation-parameters [CHECK_COMPILATION_PARAMETERS]
Default:false. Help:check compilation parameters
--keep-ninja-going [KEEP_NINJA_GOING]
Default:False. Help:When you need to debug one specific target, you can use this option to keep ninja going to skip some possible error until 1000000 jobs fail
--build-only-load [BUILD_ONLY_LOAD]
Default:False. Help:Stop build until load phase ends
--build-only-gn [BUILD_ONLY_GN]
Default:False. Help:Stop build until gn phase ends
--build-variant {user,root}
Default:'root'. Help:specifies device operating mode
--device-type DEVICE_TYPE
Default:'default'. Help:specifies device type
--disable-package-image [DISABLE_PACKAGE_IMAGE]
deprecated, please do not use this option
--archive-image [ARCHIVE_IMAGE]
Default:False. Help:archive image when build product complete
--patch [PATCH]
Default:False. Help: Apply patches as per configuration in patch.yml, and handle rollback if needed.
--rom-size-statistics [ROM_SIZE_STATISTICS]
Default:False. Help:statistics on the actual rom size for each compiled component
--stat-ccache [STAT_CCACHE]
Default:True. Help:summary ccache hitrate, and generate ccache.log in ${HOME}/.ccache dir
--get-warning-list [GET_WARNING_LIST]
Default:True. Help:You can use it to collect the build warning and generate WarningList.txt in output dir
--generate-ninja-trace [GENERATE_NINJA_TRACE]
Default:True. Help:Count the duration of each ninja thread and generate the ninja trace file(build.trace.gz)
--compute-overlap-rate [COMPUTE_OVERLAP_RATE]
Default:True. Help:Compute overlap rate during the post build
--clean-args [CLEAN_ARGS]
Default:True. Help:clean all args that generated by this compilation while compilation
finished
--deps-guard [DEPS_GUARD]
Default:True. Help:simplify code, remove concise dependency analysis, and speed up rule checking