4、编译系统入门:Gn与Ninja构建系统基础、产品配置与BoardConfig、编译命令与输出产物

好,咱们进入编译系统这一章。说实话,很多做移植的朋友,一开始都被鸿蒙的编译系统搞懵过。我当年第一次接触 OpenHarmony 时,看着那一堆 .gn 文件和 .mk 文件,心里直犯嘀咕:这玩意儿到底怎么串起来的?

别急,今天我就带你把它捋清楚。你只要搞懂三个东西:Gn 负责生成蓝图,Ninja 负责按图施工,BoardConfig 就是你的施工图纸

4.1 Gn 与 Ninja:编译系统的左膀右臂

先说说 Gn。Gn 的全称是 Generate ninja,它不直接编译代码,而是生成 Ninja 能识别的构建文件。你可以把它理解成一个「翻译官」——你把产品需求告诉 Gn,它帮你翻译成 Ninja 的指令。

核心概念:Gn 处理的是 .gn 和 .gni 文件,Ninja 处理的是 .ninja 文件。前者是高级语言,后者是汇编语言。

我个人习惯把 Gn 比作「项目经理」,Ninja 是「施工队」。项目经理(Gn)先看图纸(BUILD.gn),制定施工计划(.ninja 文件),然后施工队(Ninja)闷头干活,只管编译,不问为什么。

为什么会这样设计?因为 Ninja 的设计哲学就是「快」。它不做任何依赖分析,只执行已经算好的指令。Gn 则负责那些复杂的依赖计算和条件判断。你想想看,如果每次编译都要重新分析依赖,那得慢成什么样?

4.1.1 Gn 的基本语法

来看一个最简单的 BUILD.gn 文件:

# 定义一个可执行文件目标
executable("hello_world") {
  sources = [
    "main.c",
    "utils.c"
  ]
  
  include_dirs = [
    "include",
    "//third_party/xxx/include"
  ]
  
  deps = [
    "//base/startup:libinit",
    "//utils/native:libutils"
  ]
}

嗯,这里要注意:// 表示从源码根目录开始找路径。这个和 Linux 的绝对路径有点像,但又不完全一样。我在项目中遇到过好几次,新人把 // 当成注释了,结果编译报错半天找不到原因。

避坑指南:我曾经在移植某款开发板时,因为漏写了一个 //,导致 Gn 找不到依赖库,整整排查了两个小时。后来我养成了一个习惯:所有路径引用,要么用 // 绝对路径,要么用 . 相对路径,绝不混用。

4.1.2 Ninja 的执行逻辑

Ninja 本身没什么好讲的,它就是个执行器。你只需要知道几个常用命令:

# 执行编译
ninja -C out/产品名

# 只编译某个模块
ninja -C out/产品名 模块名

# 查看编译依赖图
ninja -C out/产品名 -t graph 模块名 | dot -Tpng -o graph.png

说实话,Ninja 的 -t graph 功能我用的不多,但有一次调试依赖问题时,它帮了大忙。当时有个模块总是被重复编译,我画了个依赖图一看,好家伙,循环依赖了。

4.2 产品配置与 BoardConfig

产品配置,说白了就是告诉编译系统:你要给谁编译?用什么芯片?带哪些功能?

在 OpenHarmony 里,产品配置主要涉及两个文件:

文件 作用 典型路径
product.gni 定义产品名称、版本、特性开关 productdefine/common/products/xxx.json
BoardConfig.mk 定义硬件相关的编译参数 device/厂商/开发板/BoardConfig.mk

4.2.1 BoardConfig.mk 详解

BoardConfig.mk 是移植工作中最常打交道的文件。它决定了你的内核用什么配置、uboot 怎么编、分区怎么划分。

我随便拿一个实际项目的 BoardConfig.mk 片段给你看:

# 芯片架构
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := cortex-a55

# 内核相关
LINUX_KERNEL_VERSION := 5.10
LINUX_KERNEL_CONFIG := device/xxx/kernel/config

# 分区大小(单位:MB)
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1024
BOARD_USERDATAIMAGE_PARTITION_SIZE := 2048
BOARD_CACHEIMAGE_PARTITION_SIZE := 256

# 编译工具链
TOOLCHAIN_PREFIX := aarch64-linux-gnu-

你想想看,这些参数一旦配错,轻则编译不过,重则刷机后系统起不来。我记得有一次,我把 TARGET_ARCH 写成了 arm(32位),但芯片明明是 arm64 的。结果编译出来的内核在开发板上直接 panic,连 log 都来不及看就重启了。

警告:BoardConfig.mk 里的分区大小一定要和实际 flash 匹配。我曾经见过有人把 system 分区设得比 flash 还大,编译倒是过了,但刷机时直接报错「空间不足」。这种问题排查起来特别费劲,因为编译系统不会检查分区大小是否合理。

4.2.2 产品特性配置

除了硬件参数,产品配置还涉及功能裁剪。比如你的设备不需要蓝牙,那就关掉蓝牙相关的 feature:

# productdefine/common/products/my_product.json
{
  "product_name": "my_product",
  "version": "1.0.0",
  "features": {
    "bluetooth": false,
    "wifi": true,
    "gps": false,
    "sensor": true
  }
}

这些 feature 开关会通过 Gn 的 declare_args() 传递到各个模块的 BUILD.gn 里。说白了,就是全局变量。

4.3 编译命令与输出产物

好,到了最实操的部分。编译命令其实就那么几条,但每条背后的含义你得清楚。

4.3.1 标准编译流程

# 第一步:设置环境
source build/envsetup.sh

# 第二步:选择产品
hb set

# 第三步:开始编译
hb build -f

嗯,这里要注意:hb 是鸿蒙的编译封装工具,它底层调用的还是 Gn 和 Ninja。我个人习惯直接看 hb 的日志,因为它会把 Gn 和 Ninja 的调用参数都打印出来。

如果你想跳过 hb,直接调用底层命令,也可以:

# 手动执行 Gn
gn gen out/my_product --args="product_name=\"my_product\" build_type=\"release\""

# 手动执行 Ninja
ninja -C out/my_product

4.3.2 输出产物说明

编译完成后,输出产物都在 out/产品名/ 目录下。我整理了一个表格,方便你对照:

产物 路径 说明
内核镜像 out/产品名/kernel/ 编译好的 Linux 内核,通常是 Image 或 zImage
系统镜像 out/产品名/packages/phone/system.img 包含所有系统服务的 ext4 镜像
用户数据镜像 out/产品名/packages/phone/userdata.img 用户分区镜像,初始为空
Bootloader out/产品名/packages/phone/uboot.img 引导加载程序
编译日志 out/产品名/build.log 完整的编译过程日志,排查错误首选

个人经验:我每次编译完,第一件事不是刷机,而是去检查 build.log 的最后几行。如果看到 ninja: no work to do,说明编译成功了。如果看到 FAILED,那就得往回翻,找到第一个错误。记住,只看第一个错误,后面的往往是连锁反应。

4.3.3 增量编译与清理

开发过程中,我们不会每次都全量编译。Ninja 支持增量编译,只重新编译修改过的文件:

# 增量编译(默认行为)
hb build

# 强制重新编译某个模块
hb build -T 模块名

# 清理编译产物
hb clean

说实话,增量编译是 Ninja 最让我省心的功能。有一次我改了一行驱动代码,全量编译要 40 分钟,增量编译只用了 2 分钟。你想想看,这效率差距有多大。

但要注意,有些情况下增量编译会出问题。比如你改了 BoardConfig.mk 里的分区大小,或者改了全局的 feature 开关,这时候最好 hb clean 一下再全量编译。我曾经因为偷懒没 clean,结果刷机后系统分区大小还是旧的,折腾了半天才发现是缓存的问题。

4.4 本章小结

这一章的内容,说白了就是让你明白三件事:

  • Gn 是翻译官,把产品需求转成 Ninja 指令
  • BoardConfig 是硬件身份证,告诉编译系统你的板子长什么样
  • 编译命令是操作手柄,掌握 hb buildhb clean 就够用了

下一章,我们会真正开始动手移植。到时候你会发现,今天学的这些编译知识,就是移植工作的地基。地基打不牢,楼盖得再高也得塌。