本文摘录自 OHOZ 团队的 OpenHarmony 源码导读项目,在线阅读(腾讯云、Github Pages)中包含最新的内容。
鸿蒙的编译构建子系统
鸿蒙中可以使用多种工具进行编译,可以将其分为高、中、低三层:
几种工具的对比:
Build Tool | 开发语言 | 开发方 | 资源 |
---|---|---|---|
hpm | js | HW | |
hb | python | HW | gitee |
gn | C++/Python | o-lim | github |
ninja | C++/Python/C | ninja | github、Doc |
我们先从底层说起。
gn 和 ninja
说实话,理解 gn 和 ninja 对于没有接触过 make、cmake 的同学是有困难的,很难理解这些跨平台工具出现的真正意义及其要解决的问题是什么。更不要说长期使用 VS、Eclipse、XCode 等成熟 IDE 的同学,这些过程都被 IDE 屏蔽掉了,但在 Linux 和嵌入式开发中它有是空气和水一般的存在,所以,嗯……尽力吧。
编译系统从 make 到 cmake 至 gn+ninja,编译器(前后端工具)从 gcc 到 gcc+llmv 至 clang+llvm,这么多年来经历的变迁不是很多,至少相比各种编程语言的变迁少太多了。
ninja(忍者),google chromium 团队出品,致力于比 make 更快的编译系统,ninja 像是编译器(Compiler)的预处理器,主要目的是递归查找好依赖关系,提前建立依赖树,gcc 可按照依赖树依次编译,大大减少编译期间杂乱的编译顺序造成的查找和重复时间。ninja 首次在 2016 年的 Android N 中使用,当前被广泛应用在希望从编译耗时中解脱出来的大型项目中。
gn 意思是 generate ninja,即生成 Ninja 所需的文件(meta data),所以 gn 自称为元数据构建(meta-build)系统,也是 google chromium 团队出品,gn 的文件后缀为 .gn
、.gni
。
gn 类似 cmake,ninja 类似 make,cmake 最终也是生成 makefile,gn 则会生成 ninja 文件,都是为了减少手工写 make/ninja 文件的工作量。
如果使用 harmony 提供的 docker,gn 和 ninja 都已经安装好了:
root@90065f887932:/home/openharmony# gn --version
1717 (2f6bc197)
root@90065f887932:/home/openharmony# ninja --version
1.9.0
gn
由于特殊原因,以下资源都都需要*:
- gn 官网: https://gn.googlesource.com/
- git 库:
git clone https://gn.googlesource.com/gn
- 在线文档:docs、reference
- 版本下载:Linux、macOS、windows
如果不方便*,可以 gitee 上搜索 gn 或 generate-ninja,可以看到网友搬运过来的,比如笔者搬运的 gn,其中 docs 和 examples 目录可以参考。
命令与流程
gn 的准备工作是这样的:
- 首先你要按照 gn 的语法在根目录手写一个
.gn
文件,,它没有文件名,只有扩展名,并且在里面至少定义buildconfig
变量,这个变量的值是一个 config 文件的路径 - 这个 config 文件当然也是你手写出来的,该文件主要完成 2 件事:
- 通过
set_defaults
函数为 4 类编译目标(executable、static_library、shared_library、source_set)定义默认配置参数 - 通过
set_default_toolchain
、tool()
……函数定义默认的 toolchain
- 通过
- 最后你还要在根目录下再手写一个 BUILD.gn 文件,这个文件真正主要也是完成 1 件事:指定编译目标,即上面的 4 类目标中的一个或几个,包括:
- 指定要编译的文件
- 指定 include 文件的路径
- 指定依赖的编译目标
准备工作完成后,就可以在命令行执行编译了:
-
gn gen out/xxx
: 生成 ninja 能够使用的构建文件- 在当前目录(找不到就向上找)或
--root
指定目录 或--dotfile
指定文件查找.gn
,找到后将其所在路径设为 root 或.gn
中root
指定的路径设置为 root,Harmony 通常是root=
指定根目录为build/lite/.gn
。 - 解析 root 下的 gn 文件以获取 buildconfig 文件名称,执行 buildconfig 文件得到 toolchain 及其 configs。
- 解析 root 下的
BUILD.gn
文件,加载其依赖的其它目录下的BUILD.gn
文件- BUILD.gn 一般作为模块的工程入口文件,可以配合.gni 文件来划分源码或模块。
- 当多个模块之间差异很小时,可以利用 BUILD.gn 来统一管理这些模块的设置,利用.gni 来专属负责各个模块源文件管理。
- 惯例使用
out/xxx
来存放编译出.ninja 文件,比如:out/debug
、out/v0.1
。 - 编译出的 ninja 文件可以使用
ninja -C out/xxx
来完成真正的版本编译。 - Tips
-
gn gen
还可以针对 IDE 的生成工程文件,可以通过--ide
来指定,比如:gn gen --ide [vs|xcode|eclipse|qtcreator|json]
-
- 在当前目录(找不到就向上找)或
-
check
: Check header dependencies. -
ls
: List matching targets. -
format
: Format .gn files. -
refs
: Find stuff referencing a target or file. -
clean
: Cleans the output directory.
更多详细的子命令可以查看
gn help
Commands 章节。
下面我们来看 gn 文件的语法:
类型与变量
gn 是一门简单的动态类型语言,有变量,变量支持的数据类型有:布尔、有符号数、字符串、列表、作用域(类似字典),用户自定义变量之外,gn 还内建了 20+ 个变量,比如:
- current_cpu、current_os、current_toolchain
- target_cpu、target_os、target_name、target_out_dir
- gn_version
- python_path
更多详细的 gn 变量可以查看
gn help
Built-in predefined variables 章节。
gn 的变量的定义和使用都很直白:
board_arch = "arm" # 定义变量
if (board_arch != "") { # 使用变量
cflags += [ "-march=$board_arch" ] # 字符串中使用变量
cflags_cc += [ "-march=${board_arch}" ] # 加上 {} 是等效的
}
标识是有格式要求的字符串,最终形成的依赖关系图中所有的元素(目标 Target、配置、工具链)都由标识做唯一识别,它格式要求是:
"//<path>[:<name>][(<toolchain>)]"
除了 path 不能省略外,其他都能省,如果 name 省略了则标识与 path 最后一个字段同名的那么,举例:
-
"//base/test:test_support(//build/toolchain/win:msvc)"
最完整格式,定位到root/base/test/BUILD.gn
文件中的test_support
"//base/test:test_support"
-
"//base/test"
等价与"//base/test:test"
函数
gn 支持简单的控制语句,如:if…else、foreach 等,gn 也支持函数,并且内建了很多函数,一般很少见用户自定义函数,估计内建函数已经足够使用了吧。
gn 的函数命名和参数传递与 c、python 等编程语言的不同,参数传递使用 invoker 来传递。
gn 有 30+ 个内建函数,包括:
-
import
:引入一个文件,但与 c/c++ 的 include 不同,import 的文件将独立执行并将执行结果放入当前文件。 -
getenv
:获取环境变量 -
print
:不解释 -
read_file
、write_file
-
foreach
:迭代一个 list -
config
:定义 configuration 对象 -
set_defaults
:定义某个 target 的成员变量默认值 -
template
:定义一套 rule,调用 rule 能够生成一个 target
更多详细的内建 functions 可以查看
gn help
Buildfile functions 章节。
举例:如果我们希望定义一些配置数据(并且有嵌套),然后赋值给某个变量,可以这样实现:
# build/config/BUILD.gn
config("cpu_arch") { # 用 config 函数定义一个名为 cpu_arch 的配置对象
cflags = [ "-march=$board_arch" ]
}
config("ohos") { # 定义一个名为 ohos 的配置对象
configs = [
":cpu_arch", # 包含上面 cpu_arch 的配置对象
":stack_protector",
]
}
然后就可以使用标识将配置对象赋值给变量
default_target_configs = [ "//build/config:ohos" ]
目标/功能块/Target
gn 中还有个重要概念:target,有些地方翻译成目标,我觉得不是很准确,它是构造表中的一个节点,它含有一些变量,以完成一些操作,变量就像是操作的配置数据,target 就像是一段封装好的操作模块——所以我觉得翻译成功能块更合适些。target 的写法是:
<target>("<name>") {
<var> = ...
}
举例:copy target 可以根据 sources 和 outputs 变量实现文件拷贝:
copy("compiler") {
sources = [
"//prebuilts/gcc/linux-x86/arm/arm-linux-ohoseabi-gcc/arm-linux-musleabi",
"//prebuilts/gcc/linux-x86/arm/arm-linux-ohoseabi-gcc/arm-linux-ohoseabi",
]
outputs = [ "$ndk_linux_toolchains_out_dir/{{source_file_part}}" ]
}
举例:action target 可以完成 script 变量指定的脚本,Harmony 中 build/lite/BUILD.gn
中生成跟文件系统的操作,使用了 action target:
action("gen_rootfs") {
deps = [ ":ohos" ]
script = "//build/lite/gen_rootfs.py" # 执行此 python 文件
outputs = [ "$target_gen_dir/gen_rootfs.log" ] # 输出 log 文件
out_dir = rebase_path("$root_out_dir")
args = [ # python 文件可以接受的命令行参数
"--path=$out_dir",
"--kernel=$ohos_kernel_type",
"--storage=$storage_type",
"--strip_command=$ohos_current_strip_command",
"--dmverity=$enable_ohos_security_dmverity",
]
}
由于 gn 就是 python 写的,所以可以完美的兼容 python 脚本来执行操作。
举例:source_set
是非常关键的一个 target,定义了源码集,gn 会对其逐一生成 .o 文件,其中 configs 变量定义了编译时送给编译器的参数。比如前文已经定义好了 default_target_configs
变量,现在就可以使用 set_defaults
函数中来定义 source_set
target 中的 configs 变量了。
set_defaults("source_set") {
configs = default_target_configs
}
至于使用哪些编译器,gn 使用 set_default_toolchain
函数定义:
set_default_toolchain("//build/lite/toolchain:gcc-arm-none-eabi")
举例:gn help template
给出了一个例子,非常典型的使用了
- 函数:template、assert、get_target_outputs
- Target:action_foreach、source_set、executable
首先定义使用 template 定义一个 rule,叫做 my_idl:
template("my_idl") {
# 先对入参做一个判断,以免报错,对使用者抛error是没有给出错误提示来的优雅。
assert(defined(invoker.sources),
"Need sources in $target_name listing the idl files.")
# 定义一个过程变量
code_gen_target_name = target_name + "_code_gen"
# action_foreach 是个 target,能够根据 {...} 内的参数执行 script 指定的脚本
# 本 action 是希望把 idl 文件生成出 .cc 和 .h 文件
action_foreach(code_gen_target_name) {
sources = invoker.sources
script = "//tools/idl/idl_code_generator.py"
# 告诉 gn 如何存放 output 文件,"gn help source_expansion" 有更多细节
outputs = [ "$target_gen_dir/{{source_name_part}}.cc",
"$target_gen_dir/{{source_name_part}}.h" ]
}
source_set(target_name) {
sources = get_target_outputs(":$code_gen_target_name")
deps = [ ":$code_gen_target_name" ] # 指定其依赖上面 action_foreach(code_gen_target_name)
}
}
然后看如何使用(invoking)template:
my_idl 是上面 template 定义的一个 rule,rule 使用起来像函数,但千万不要把对应位置理解成入参和函数体,rule 的这 2 个位置分别放置:target 名称、入参。
# 由 rule 生成一个名为 foo_idl_files 的 target
my_idl("foo_idl_files") {
# 这里定义的变量会转发给 rule 定义中,使用 invoker.xxx 引用,所以这是入参
sources = [ "foo.idl", "bar.idl" ] # 对应 template 中的使用 invoker.sources
}
我们可 gn 内建 target 的使用,也是 <target_type>(<target_name>) { <target_invoker>}
:
executable("my_exe") {
deps = [ ":foo_idl_files" ] # my_exe 这个 target 依赖 foo_idl_files 这个 target
}
gn 默认定义了很多 Target(功能块),比如:
- 执行某个或某组动作
- action:单次运行的动作
- action_foreach:在多个文件中依次运行脚本的 target
- copy:执行 copy 动作
- 生成最终目标 —— 下面这 4 个非常必要,必须选择一个或多个进行定义
- excutable:指定 target 是个可执行文件
- source_set:定义源码集,会逐一对应生成
.o
文件,即尚未链接(link)的文件 - shared_library、static_library:声明一个动态(win=
.dll
、linux=.so
)、静态库(win=.lib
、linux=.a
)
- 辅助类
- group:声明一个 target
每个 targte 都有自己的用法,ge help <target>
查看,里面都会有一段 demo 代码可以拿来直接使用。
更多详细的 Target 可以查看
gn help
Target declarations 章节。
args 传参
用户如何向 gn 传递项目自定义的参数呢?有这样几种方式:
$ gn gen out/debug --args="key=int_value key1=\"str_value\" key3=\"$(PWD)\""
gn gen
时通过 --args
传递进去,因为后面整体被看做一个参数,所以需要用 ""
引号整体包围,如果 value 是数字,可以不加 \"
,如果是字符串或 shell 变量,则需要添加 \"
。
$ gn args out/debug [--args=...]
gn args
会先在 out/debug 下创建 args.gn 文件,然后用编辑器打开(就行 git commit 打开一个编辑器让用户填写 log 一样),等待用户添加 key = value
。同时 --args
也可以添加到命令行里,规则和上面一样。
gn 内建了 6 个变量,即:即使用户不做任何指定、赋值,.gn
和 xxx.gni
、xxx.gn
都可以使用,gn 会自定义赋值的:
- host_cpu
- host_os
- current_cpu
- current_os
- target_cpu
- target_os
root 下的 .gn
文件也可以使用 default_args 变量
为这 6 个变量赋默认值,比如指定不同的 target_cpu:
default_args = {
target_os = "freertos"
target_cpu = "cortex-m4"
}
用户自定义的 arg,则需要多一个步骤,在 BUILDCONFIG.gn 或 BUILD.gn (取决于用户希望起效的作用域)中使用 declare_args()
函数进行定义,并赋默认值,如:
declare_args() {
ohos_build_type = "debug"
}
然后,.gn
和 xxx.gni
、xxx.gn
中就可以像普通变量一样始终这些 args 了:
if (target_os == "linux") {
...
}
if (ohos_build_type == "debug") {
...
}
掌握 gn 的命令、函数、变量、标识、Target……等概念,基本就能够使用 gn 完成任务了。我们在后续 hb 章节里则会去分析具体的 Harmony 代码了。
ninja
gn 会在 out/<target>/<board>/
下生成 build.ninja 文件,这是个普通的文本文件,打开之后甚至觉得就是个 log 文件,这估计就是 ninja 创始人说的极简哲学吧。
$ ninja -C out/<target>/<board>/
可以编译出目标文件或可执行文件。
参考:
build_lite 与 hb
本章节解析的组件及其对应的目录、git 库如下:
hpm 组件名 | 源码目录 | gitee url |
---|---|---|
@ohos/build_lite | build/lite | https://openharmony.gitee.com/openharmony/build_lite |
Harmony 还另外开源了一个 build 组件:
hpm 组件名 | 源码目录 | gitee url |
---|---|---|
@ohos/build | build | https://openharmony.gitee.com/openharmony/build |
build_lite 是为编译嵌入式单板准备的,build 是为编译 Phone 等智能终端准备的。
build_lite 主要功能实现了一个名为 hb
的 Python 包,能够安装到 Python 环境中,除此之外,还包括:
- 描述文件(.json)
- components:组件描述文件
- 制作脚本(.sh)
- make_rootfs:文件系统制作脚本
- 配置文件(.gn、.gni)
- config:编译相关的配置项(组件、内核、子系统)
- ndk:Native API 相关编译脚本与配置参数
- toolchain
- xx 文件(.ld)
- platform:
gn 总入口
鸿蒙把 build/lite
独立出一个开源项目,将其设计为 gn 的 root,已经替我们生成好了相关的 gn 文件,build/lite/.gn
文件就是 gn 编译 Harmony 的总入口,让我们一探其源码吧。
.gn
和 BUILDCONFIG.gn
里面只有 2 句:
buildconfig = "//build/lite/config/BUILDCONFIG.gn"
root = "//build/lite"
BUILDCONFIG.gn 代码关键架构:
import("//build/lite/ohos_var.gni")
import("${device_path}/config.gni")
target_os = "ohos" # 这是个 gn 内建变量,对其赋值
target_cpu = board_cpu # 这是个 gn 内建变量,对其赋值
# 配置 toolchain
if (board_toolchain != "" && use_board_toolchain) { # 使用 device 指定的 toolchain
# 其中会用到 board_toolchain、board_toolchain_path、board_toolchain_prefix 等变量
# 这些变量都是从 import("${device_path}/config.gni") 而来的
# 最终初始化了几个核心变量
ohos_current_cc_command = "${compile_prefix}gcc"
ohos_current_cxx_command = "${compile_prefix}g++"
ohos_current_ar_command = "${compile_prefix}ar"
} else { # 使用默认 toolchain
}
上面这段定义了编译鸿蒙的 toolchain,其中 ${device_path}
是下面章节中 hb 命令送入的,一般对应整个工程源码目录下的 device/.../.../config.gni
,比如 device/hisilicon/hispark_pegasus/sdk_litos/config.gni
,其中可以看到 device 对 toolchain 的个性化定义。
BUILDCONFIG.gn 文件下面会使用 set_defaults
函数完成 executable、static_library、shared_library、source_set 四个 target 的创建。
# 定义临时变量 default_target_configs
default_target_configs += [
"//build/lite/config:board_config",
"//build/lite/config:cpu_arch",
"//build/lite/config:common",
"//build/lite/config:default_link_path",
# 下面还要几十行代码继续对 default_target_configs 追加赋值
]
# 创建 source_set target,其他 executable、static_library、shared_library 同理
set_defaults("source_set") {
configs = default_target_configs
}
"//build/lite/config:cpu_arch"
是前文所说的标识,表示从 build/lite/config
下的 BUILD.gn 文件中提取 cpu_arch
对象,它的定义
config("cpu_arch") {
cflags = []
if (board_arch != "") {
cflags += [ "-march=$board_arch" ]
}
if (board_cpu != "") {
cflags += [ "-mcpu=$board_cpu" ]
}
cflags_cc = cflags
ldflags = cflags
}
这是一个使用 config 函数定义的 object,目的是根据 board_arch 定义不同的 -mcpu
参数。
整个 BUILDCONFIG.gn 文件主要完成了 2 件事:
- 定义 toochain
- 定义 4 个 target:executable、static_library、shared_library、source_set
为后续的 gn 操作准备最基础的内容。
BUILD.gn
回忆前文 gn 的总流程,执行完 .gn
后,就要执行 root 下的 BUILD.gn 文件了。该文件整体架构也非常简单:定义 4 个 target:2 个 group、2 个 action。
import("//build/lite/ndk/ndk.gni")
# 定义 2 个 group target
group("ohos") {...}
group("ndk") {...}
# 定义 2 个 action target
if (ohos_build_target == "") {
action("gen_rootfs") {...}
if (ohos_build_type == "debug" && product != "") {
action("gen_testfwk_info") {...}
读到这里,我们可以看出,gn 是一个非常灵活的语言,写起来像 python,比 makefile 要方便的多,可以在 object 中嵌入 if…else…,使用标识可以像 url 定位一样快速引入一段其他文件定义的代码段。
OK,gn 先解读到这里,BUILD.gn
和 *.gni
文件应该已经都能看懂了,那还差一个问题:鸿蒙是最初进入 gn 的 root 之前,到底还做了什么?那就是下面 hb 要做的事情了,让我们继续往下读。
python 包 hb 及其命令行
build_lite/hb
是个 Python Package,其 setup.py 中可见其生成了一个 hb
的命令,入口地址是 build/lib/hb/__main__.py
文件中的 main 函数。
setup(
name='ohos-build',
package_dir={'hb': 'hb'},
entry_points={'console_scripts': ['hb=hb.__main__:main',]},
)
既然 build_lite 是一个普通的 Python 包,那么在实际场景中就有多种方式了:
- 获取 build_lite 源码
git clone https://openharmony.gitee.com/openharmony/build_lite
-
hpm i @ohos/hispark_pegasus
,build/lite
则已经被下载
- 安装 hb
- 可以把 hb 安装到系统的 Python 环境中:
pip install [--user] build/lite
- 可以把 hb 安装到 python 虚拟环境中:
python3 -m venv venv; source venv/bin/activate; pip install build/lite
- 由于 hb 已经是 Pypi 上的发布包,名字是
ohos-build
,所以也可以这样安装pip install [--user] ohos-build
- 如果使用
docke run ...
,则 hb 已经安装好了
- 可以把 hb 安装到系统的 Python 环境中:
- 使用 hb
- 源代码使用:
python3 -c 'import inspect,hb; print(inspect.getfile(hb))'
查看 hb 安装的路径,能够正确查询则表示可以import hb
,后续按照 python 规则开发即可。 - 命令行使用:
hb [set|env|build]
-
python build.py [build|clean|debug|release]
直接使用build/lite
目录下的 build.py 文件
- 源代码使用:
$ ls build/lite/hb
__init__.py __pycache__ clean cts env
__main__.py build common deps set
hb 将每个子命令的实现放在一个文件夹中:set、build、clean、env……
当执行 hb set
、hb build
的时候进入每个文件夹中执行 exec_command()
函数。
hb set
执行 build/lite/hb/set/set.py
中的 exec_command()
函数:
def exec_command(args):
return set_root_path() == 0 and set_product() == 0
set_root_path()
和 set_product()
分别解析出 root 路径和产品相关信息,写入 ohos_config.json 文件中。
def set_root_path(root_path=None):
config = Config()
if root_path is None:
root_path = get_input('[OHOS INFO] Input code path: ')
config.root_path = root_path
return 0
命令行里执行 hb set
给出的提示即此上面函数打印。Config
是一个单例 class(即:此函数配置的 config 实例值,其他函数都可获取):
class Config(metaclass=Singleton):
Config 单例定义了多个属性:root_path、board、kernel、product、product_path、device_path、out_path……,当做左值的时候会写入 ohos_config.json 文件。
另外一个函数 set_product()
即是为了配置 Product,Product
是 hb 为产品定义的 class,包含几个静态方法,基本都是解析出配置值,写入 ohos_config.json 文件:
$ cat common/product.py|grep -B1 'def '
@staticmethod
def get_products():
--
@staticmethod
def get_device_info(product_json):
--
@staticmethod
def get_features(product_json):
--
@staticmethod
def get_components(product_json, subsystems):
静态方法望文知意:
-
get_products()
: 获取产品信息,递归查找vender/
下包含 config.json 文件的目录,每找到一个即算一个 Product,其中的 config.json 通常包括 vender 预先定义好的发行版配置。
$ find vendor/ -name config.json
vendor/hisilicon/hispark_aries/config.json
vendor/hisilicon/hispark_pegasus/config.json
vendor/hisilicon/hispark_taurus/config.json
上面是 repo sync
获取的源码中的 vender 情况,所以在执行 hb set
时会提示 3 个选项:
$ hb set
[OHOS INFO] Input code path: .
OHOS Which product do you need? (Use arrow keys)
hisilicon
❯ ipcamera_hispark_aries
wifiiot_hispark_pegasus
ipcamera_hispark_taurus
-
get_device_info()
、get_features()
、get_components()
: 获取 vender 定义的 config.json 中的各种信息,比如:
$ cat vendor/hisilicon/hispark_pegasus/config.json | head -n18
{
"product_name": "wifiiot_hispark_pegasus",
"ohos_version": "OpenHarmony 1.0",
"device_company": "hisilicon",
"board": "hispark_pegasus",
"kernel_type": "liteos_m",
"kernel_version": "",
"subsystems": [
{
"subsystem": "applications",
"components": [
{ "component": "wifi_iot_sample_app", "features":[] }
]
},
{
"subsystem": "iot_hardware",
"components": [
{ "component": "iot_controller", "features":[] }
前面 hb set
给出的 3 个选项是这里的 product_name。device_info 包括上面的 device、board、kernel;features 和 components 是每个 subsystems 中的信息。
每个 subsystem 对应一个源代码的目录,component 是它依赖的模块,统一放在 ohos_bundles 下面。
hb build
执行 build/lite/hb/build/build.py
中的 exec_command()
函数,该函数主要处理用户的入参,如:
-
-b
:debug 或 release -
-c
:指定编译器,默认是 clang -
-t
:是否编译 test suit -
-f
:full,编译全部代码 -
-t
:是否编译 ndk,本地开发包,这也是@ohos/build_lite
组件的一部分 -
-T
:单模块编译 -
-v
:verbose
使用这些入参实例化 Build 类:
class Build():
def __init__(self):
self.config = Config()
......
def build(self, full_compile, ninja=True, cmd_args=None):
......
def check_in_device(self):
......
def gn_build(self, cmd_args):
......
def ninja_build(self, cmd_args):
......
实例化后调用 build.build()
,它会依次调用 check_in_device()
、gn_build()
和 ninja_build()
。
-
check_in_device()
:读取编译配置,根据产品选择的开发板,读取开发板 config.gni 文件内容,主要包括编译工具链、编译链接命令和选项等。 -
gn_build()
:调用 gn gen 命令,读取产品配置生成产品解决方案 out 目录和 ninja 文件。核心代码如下:gn_cmd = [gn_path, '--root={}'.format(self.config.root_path), '--dotfile={}/.gn'.format(self.config.build_path), 'clean', self.config.out_path] exec_command(gn_cmd, log_path=self.config.log_path)
-
ninja_build()
:调用 ninja -C out/board/product 启动编译。核心代码如下:ninja_cmd = [ninja_path, '-w', 'dupbuild=warn', '-C', self.config.out_path] + ninja_args exec_command(ninja_cmd, log_path=self.config.log_path, log_filter=True)
- 系统镜像打包:将组件编译产物打包,设置文件属性和权限,制作文件系统镜像。
python build.py
根目录下的 build.py 通常是 build/lite/build.py 的软连接,执行 python build.py
时会运行到 build.py 的 build()
函数:
def build(path, args_list):
cmd = ['python3', 'build/lite/hb/__main__.py', 'build'] + args_list
return check_output(cmd, cwd=path)
可见,仍是执行 hb build
,入参也可以平移过来,所以可以这么使用:
python build.py ipcamera_hi3518ev300 -b debug # 全量编译为 debug 版本
python build.py ipcamera_hi3518ev300 -T applications/sample/camera/app:camera_app # 单模块编译
可以说,build.py 实现了“不安装 hb 也能编译”的目的,其他好像没做什么。
简要流程
History
- 2020.12.05: 内核从 liteos_riscv 更名为 liteos_m,build 做适配。
$ git -P log -n1 897188
commit 8971880bd4f08a2ea01e83dfaadcf7cda7aae858
Author: p00452466 <p00452466@notesmail.huawei.com>
Date: Sat Dec 5 03:07:19 2020 +0800
Description:add Change kernel type from liteos_riscv to liteos_m
Reviewed-by:liubeibei
- 20210318: 支持独立的外接设备驱动组件编译
$ git -P log -n1 814c81
commit 814c816f9b7f900113bed0f75a8122dba5555f65
Merge: 3dc5b1d 5353b23
Author: openharmony_ci <7387629+openharmony_ci@user.noreply.gitee.com>
Date: Thu Mar 18 19:58:42 2021 +0800
!44 组件化解耦修改--支持独立的外接设备驱动组件编译
Merge pull request !44 from kevin/0316_release_build
- 2021.03.20: 本模块已经提交到 pypi,链接
$ git -P log -n1 958189
commit 95818940a0bc47d25e7454c4d37732e90f7d2df8
Author: pilipala195 <yangguangzhao1@huawei.com>
Date: Sat Mar 20 12:35:48 2021 +0800
Upload ohos_build to Pypi
- 2021.04.03: 构建不再需要先
hb set
,可以直接hb build
。
$ git -P log -n1 32d740
commit 32d7402125db0c46c43b05322e588a692f96827a
Author: SimonLi <likailong@huawei.com>
Date: Sat Apr 3 08:55:13 2021 +0800
IssueNo: #I3EPRJ
Description: build device with no need to hb set
Sig: build
Feature or Bugfix: Feature
Binary Source: No
hpm
hpm 是 2020 下半年开始,HW 开发的包管理平台,js 语言,npm 安装和更新:
$ npm install -g @ohos/hpm-cli # 安装
$ npm update -g @ohos/hpm-cli # 更新
$ npm rm -g @ohos/hpm-cli # 卸载
基本命令
-
hpm init [-t template]
在一个文件夹中初始化一个 hpm 包,主要是创建 bundle.json 文件
$ hpm init -t dist
Initialization finished.
$ cat bundle.json
{
"name": "dist",
"version": "1.0.0",
"publishAs": "distribution",
"description": "this is a distribution created by template",
}
-
hpm i|install [name]
下载依赖并安装,必须在已经hpm init
的目录下执行
$ hpm i @ohos/hispark_pegasus
-
hpm d|download [name]
仅下载指定包(tgz 文件),不下载依赖,可以在任何目录中执行
$ hpm d @ohos/hispark_pegasus
$ ls @ohos-hispark_pegasus-1.0.3.tgz
@ohos-hispark_pegasus-1.0.3.tgz
-
hpm list
打印依赖关系图
$ hpm list
+--dist@1.0.0
│ +--@ohos/hispark_pegasus@1.0.3
│ │ +--@ohos/bootstrap@1.1.1
│ │ +--@ohos/bounds_checking_function@1.1.1
-
hpm pack
打包组件(bundle),生成 tgz 文件。
$ hpm pack
> Packing dist-1.0.0.tgz /home/kevin/workspace/harmony/src/hpm.i/@hihope-neptune_iot
> directory .
> . . bundle.json
> . . README.md
> . . LICENSE
> Packing dist-1.0.0.tgz finished.
harmony 的组件(bundle)和发行版(distribution)之间是包含关系,组件由代码 + bundle.json + README + LICENSE
组成,发行版由 多个组件 + scripts
组成,官方给出的关系图:
-
hpm ui
创建 http 访问的前端,在浏览器上可查看多种信息,执行多种命令,也可以在 docker 中执行,在 host 中浏览器访问。
hpm 迭代很快,尤其是 2021.6.2 发布 Harmony2.0 以后,几天一更新,所以,即使使用 docker 容器,也建议先升级一下 hpm,以获取最新版本的特性。
源码解析
hpm 相比 hb,增加了包管理的概念,不再是纯的编译框架,hb 无法管理包之间的依赖关系,以及同一个包的多版本控制,hpm 类似 pip、npm 解决这些问题。
从 hpm-cli 在 npm 官网 上看,2020.8 提交 0.0.1 版本,但一直都没什么下载量,直到 2021.5 才开始有下载。源码暂时没找到,只能从其安装路径中看到一些:
$ hpm -V
1.2.6
$ which hpm
/home/kevin/.nvm/versions/node/v14.15.0/bin/hpm
$ ls ~/.nvm/versions/node/v14.15.0/lib/node_modules/@ohos/hpm-cli
bin hpm-debug-build.js lib LICENSE node_modules package.json README.md README_ZH.md
hpm 为每个子命令定义了一个 js 文件
$ ls ~/.nvm/versions/node/v14.15.0/lib/node_modules/@ohos/hpm-cli/lib/commands
build.js download.js init.js publish.js uninstall.js
checkUpdate.js extract.js install.js run.js update.js
code.js fetch.js lang.js script.js
config.js generateKeys.js list.js search.js
distribute.js index.js pack.js ui.js
每次执行 hpm xxx
命令,main.js 解析入参并转给相应的 command(lib/commands/xxx.js),每个命令的执行逻辑可参考代码,比如 dist 会检查 build 框架,然后交权给 build 命令,build 会先检查依赖,然后进行单线程 or 多线程编译,这里的编译依然会使用 gn 和 ninja,编译完毕后 dist 会进行打包。
History
- 1.1.0(202104):新增 GUI,
hpm ui
启动 - 1.2.3(202106):新增
fetch
、download
、code
子命令
DevEco Device Tool
HUAWEI DevEco Device Tool(下文简称 DDT)是 HarmonyOS 面向智能设备开发者提供的一站式集成开发环境,它比 hpm 提供了更多的功能:组件按需定制,支持代码编辑、烧录和调试等。
所以 DDT 已经不再局限与本文所讨论的编译,但 DDT 的编译过程又比较特殊,它更加灵活的使用 hb、hpm 等工具,并又开发了一个 hos。当你使用 DDT build 的时候,执行了这个命令:
/home/kevin/.deveco-device-tool/core/deveco-venv/bin/hos run --project-dir /home/kevin/workspace/harmony/src/bearpi --environment bearpi_hm_nano
DDT 安装在 ~/.deveco-device-tool
,主要含 3 个文件夹:core、platforms、plugins
core 包含了编译、调试、烧录工具,和 python 的虚拟环境:
$ ls .deveco-device-tool/core
arm_noneeabi_gcc deveco-venv tool_hiburn tool_openocd
contrib_pysite feature-toggling-manifest.json tool_lldb tool_scons
deveco-home tool_burn tool_openlogic_openjdk_jre tool_trace
platforms 包含针对不同 SoC 厂家的编译工具,海思、联盛德、NXP……每家一个文件夹,大多是 python 实现,其中有些含 hb.py,有些没有 hb,看来定制化已经让编译工具五花八门,HW 也不管了,自家分开玩儿吧。
$ ls .deveco-device-tool/platforms
asrmicro bestechnic blank bouffalo hisilicon nxp realtek winnermicro xradio
其中的 asrmicro(翱捷科技)、bestechnic(恒玄科技)、bouffalo(博流科技)、xradio(芯之联)都还没见到其开发板,应该在开发中或已经 alpha 状态了。
plugins 中包含 VSCode 的扩展文件
$ ls .deveco-device-tool/plugins
deveco-device-tool-2.2.0+285431.76f4090e.vsix
由于 DDT 既不开源,也缺乏文档,所以暂时很难解读,以后再说。
官方资源:
总结
总体流程图
下载方式-编译方式对比表
对比项 | HarmonyOS (repo) | neptune (hpm) | pegasus (hpm) | 3861 (DDT) | bearpi (DDT) | 3516/8 (DDT) |
---|---|---|---|---|---|---|
别称 | * | HH-SLNPT10x | Hi3861V100 | |||
SoC | * | WinnerMicro W800 | Hi3861 | Hi3861 | Hi3861 | Hi3516/18 |
SoC Kernel | * | 玄铁 804(RISC-V) | RISC-V | 同左 | 同左 | Cortex-A7 |
外设 | * | 2MB(F)+288KB® | ||||
特色 | * | WiFi、BT | 2.4GHz WiFi | 同左 | ||
Vendor | * | 润和(hihope) | 海思(HiSili) | 同左 | 小熊派 | |
/build.py |
Y | - | - | |||
/.deveco |
- | - | - | Y | Y | |
/.vscode |
- | - | - | Y | Y | |
/device |
Y | [Y] | - | |||
/vendor |
Y | - | Y | |||
/build/ |
Y | Y | Y | Y | - | - |
/build/lite/hb |
Y | - | Y | - | - | - |
hb build |
Y | |||||
python build.py |
Y | |||||
hpm dist |
Y | Y | ||||
DDT build |
Y | Y | Y |
- DDT 方式下载的有
.deveco
和.vscode
文件夹,编译也需要.deveco
。 - DDT 可以使用已经安装的 platforms 中的 build 工具,所以
build/lite
都不需要了,hb build
和python build.py
也不可用了。
参考
- pegasus: 飞马、天马
- neptune:海王星
- taurus:金牛座
- aries:白羊座
- WinnerMicro:北京联盛德微电子