构建系统核心概念:gn与ninja简介

说到鸿蒙的编译系统,绕不开两个工具:gnninja

我第一次接触它们时,心里也犯嘀咕——这俩是干嘛的?后来在项目中踩了几次坑,才真正理解它们的定位。

简单说,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 文件 你编写的构建规则 gn 工具 生成 .ninja 文件 *.ninja 文件 中间构建描述 ninja 工具 执行编译 最终产物 .so / .bin / .hap 配置阶段(一次) 编译阶段(每次) 工作流程说明: 1. 开发者编写 BUILD.gn 文件,描述模块、依赖、编译选项 2. gn 工具解析 BUILD.gn,生成高效的 .ninja 中间文件 3. ninja 工具读取 .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