鸿蒙源码导读-02:编译构建子系统

本文摘录自 OHOZ 团队的 OpenHarmony 源码导读项目,在线阅读(腾讯云Github Pages)中包含最新的内容。


鸿蒙中可以使用多种工具进行编译,可以将其分为高、中、低三层:
在这里插入图片描述

几种工具的对比:

Build Tool 开发语言 开发方 资源
hpm js HW
hb python HW gitee
gn C++/Python o-lim github
ninja C++/Python/C ninja githubDoc

我们先从底层说起。

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
  • 在线文档:docsreference
  • 版本下载:LinuxmacOSwindows

如果不方便科学上网,可以 gitee 上搜索 gn 或 generate-ninja,可以看到网友搬运过来的,比如笔者搬运的 gn,其中 docs 和 examples 目录可以参考。

命令与流程

gn 的准备工作是这样的:

  1. 首先你要按照 gn 的语法在根目录手写一个 .gn 文件,,它没有文件名,只有扩展名,并且在里面至少定义 buildconfig 变量,这个变量的值是一个 config 文件的路径
  2. 这个 config 文件当然也是你手写出来的,该文件主要完成 2 件事:
    • 通过 set_defaults 函数为 4 类编译目标(executable、static_library、shared_library、source_set)定义默认配置参数
    • 通过 set_default_toolchaintool()……函数定义默认的 toolchain
  3. 最后你还要在根目录下再手写一个 BUILD.gn 文件,这个文件真正主要也是完成 1 件事:指定编译目标,即上面的 4 类目标中的一个或几个,包括:
    • 指定要编译的文件
    • 指定 include 文件的路径
    • 指定依赖的编译目标

准备工作完成后,就可以在命令行执行编译了:

  • gn gen out/xxx: 生成 ninja 能够使用的构建文件
    • 在当前目录(找不到就向上找)或 --root 指定目录 或 --dotfile 指定文件查找 .gn,找到后将其所在路径设为 root 或 .gnroot 指定的路径设置为 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/debugout/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_filewrite_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
  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值