构建系统核心概念:gn与ninja简介
说到鸿蒙的编译系统,绕不开两个工具:gn 和 ninja。
我第一次接触它们时,心里也犯嘀咕——这俩是干嘛的?后来在项目中踩了几次坑,才真正理解它们的定位。
简单说,gn 负责「生成构建规则」,ninja 负责「执行构建任务」。
gn 全称 Generate Ninja,它读取 .gn 和 BUILD.gn 文件,生成 .ninja 文件。ninja 再根据这些 .ninja 文件,调用编译器、链接器完成编译。
为什么不用 Make?我个人觉得,Make 在大规模项目里太慢了。鸿蒙源码几万个文件,Make 光解析依赖关系就要半天。ninja 的设计目标就是「快」,它直接执行,不做额外解析。
核心关系:
BUILD.gn(你写的构建规则)→ gn 工具 → *.ninja(中间文件)→ ninja 工具 → 最终产物(.so/.bin/.hap)
嗯,这里要注意:gn 只在配置阶段运行一次,ninja 在每次编译时运行。所以修改了 BUILD.gn 后,需要重新执行 gn 生成 ninja 文件。
BUILD.gn 文件结构
BUILD.gn 是 gn 的构建描述文件。你想想看,它其实就是一个声明式的配置文件,告诉 gn 你要编译什么、怎么编译。
一个典型的 BUILD.gn 长这样:
# 声明一个可执行文件目标
executable("hello_world") {
sources = [
"main.c",
"utils.c",
]
include_dirs = [
"//third_party/openssl/include",
]
deps = [
"//base/utils:libutils",
"//third_party/openssl:openssl",
]
cflags = [ "-Wall", "-O2" ]
}
我来拆解一下这个结构:
- executable:目标类型,表示生成可执行文件。还有 shared_library(动态库)、static_library(静态库)、source_set(源码集合)等。
- sources:源文件列表。我习惯把 .c/.cpp 文件都列在这里。
- include_dirs:头文件搜索路径。注意用
//表示项目根目录。 - deps:依赖列表。格式是
//路径:目标名。 - cflags:编译选项。可以加 -Wall、-O2 这些。
个人经验:sources 里不要用通配符 *.c。我曾经这么干过,结果新增文件时忘了重新 gn gen,编译死活找不到新文件。后来老老实实一个个列出来,反而更清晰。
target 与 action
target 是 gn 里的基本构建单元。说白了,一个 target 就是一个「我要造什么东西」的声明。
常见的 target 类型:
| 类型 | 说明 | 产物 |
|---|---|---|
| executable | 可执行文件 | .bin / 无后缀 |
| shared_library | 动态链接库 | .so |
| static_library | 静态链接库 | .a |
| source_set | 源码集合(不打包) | 无独立产物 |
| action | 自定义脚本动作 | 自定义 |
action 比较特殊。它不是编译代码,而是执行一段脚本。比如生成头文件、处理资源文件等。
action("generate_version") {
script = "//build/scripts/gen_version.py"
outputs = [ "$target_out_dir/version.h" ]
args = [ "--output", rebase_path("$target_out_dir/version.h") ]
}
嗯,这里要注意:action 的 outputs 必须写清楚,ninja 靠它判断是否需要重新执行。
我曾经踩过的坑:action 里忘记写 outputs,结果每次编译都重新执行脚本,白白浪费几十秒。后来检查文档才发现,outputs 是 action 的「缓存依据」。
依赖管理
依赖管理是构建系统的核心。你想想看,一个模块可能依赖几十个库,这些库又依赖其他库。如果依赖关系乱了,编译就会报错。
gn 的依赖管理通过 deps 字段实现:
shared_library("network_module") {
deps = [
"//base/security:security_lib",
"//base/communication:comm_lib",
"//third_party/jsoncpp:jsoncpp",
]
}
依赖的写法格式://路径:目标名
//表示项目根目录- 路径是相对于根目录的 BUILD.gn 所在目录
- 目标名是 BUILD.gn 里定义的 target 名称
举个例子://base/security:security_lib 表示根目录下 base/security/BUILD.gn 文件里定义的 security_lib 目标。
gn 会自动处理传递依赖。也就是说,你只需要声明直接依赖,gn 会递归解析所有间接依赖。
依赖管理原则:
- 只声明直接依赖,不要写间接依赖
- 避免循环依赖(A 依赖 B,B 又依赖 A)
- 公共库尽量用 source_set,减少链接时间
我个人习惯把依赖按功能分组,用注释标明用途:
deps = [
# 安全相关
"//base/security:sec_core",
"//base/security:crypto",
# 网络相关
"//base/net:http_client",
"//base/net:websocket",
# 第三方库
"//third_party/protobuf:protobuf_lite",
]
这样写,别人看代码时一目了然。我在项目里带新人时,就要求他们按这个规范来写。
还有一个实用技巧:用 public_deps 声明「公开依赖」。什么意思呢?如果你的模块 A 依赖了库 B,而使用 A 的人也需要包含 B 的头文件,那就用 public_deps。
shared_library("my_module") {
public_deps = [ "//base/utils:utils" ]
deps = [ "//base/internal:internal_lib" ]
}
这样,依赖 my_module 的人会自动获得 utils 的依赖,但不会获得 internal_lib 的依赖。说白了,public_deps 就是「传递给你的使用者」的依赖。
避坑指南:不要滥用 public_deps。我曾经在一个项目里把所有 deps 都写成 public_deps,结果依赖关系变得一团糟,编译时大量重复链接。后来花了一整天重构才理顺。
好了,关于 gn 和 ninja 的核心概念就聊到这里。这些是理解鸿蒙编译系统的基础,后面我们会深入具体案例。
公众号:蓝海资料掘金营,微信deep3321