本文摘录自 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
:迭代一个 listconfig
:定义 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