GN (generate ninja) 学习手册

阅读建议: 通过第一节 "GN概述",和第二节给出的简单示例,掌握GN的基本概念和工作流程。第三节给出了GN的详细语法和常用内容,可做概要了解。在实际阅读、编写gn文件时,可通过 gn help 命令查询内置目标、变量、方法的详细用法。

1. GN概述

1.1 GN是什么

1.1.1 基本定义

GN(Generate Ninja)是一款由Google开发的元构建系统 (meta-build system),或称为构建文件生成器 。它的核心设计目标是:快速生成高效、正确的Ninja构建文件,从而驱动Ninja构建系统执行实际的编译和链接任务。

GN本身并不直接编译任何代码。其工作流程明确分为两个阶段:

  1. 生成阶段 :GN读取用其专用语法编写的项目配置文件(如.gnBUILD.gn),解析目标、依赖关系和构建参数,生成一个高度优化的、纯文本的build.ninja文件。
  2. 构建阶段 :Ninja读取build.ninja文件,以最小的开销和最大化的并行度,调用编译器、链接器等工具链命令,执行实际的构建工作。

1.1.2 核心特点

  1. 极致的速度:GN本身使用C++编写,其核心算法针对超大型项目(如Chromium)进行了优化。它能在数秒内完成包含数十万个构建目标的依赖图解析和文件生成。
  2. 可读性强的声明式语法 :GN采用类似Python的简洁、缩进敏感语法,鼓励清晰、扁平的代码结构。其语法是声明式的,开发者描述"构建什么"以及"依赖什么",而不是编写一步步的操作指令。
  3. 确定性与可重现性:相同的源代码和构建参数输入,GN总会生成完全相同的Ninja文件,确保了构建的可重现性,这对持续集成和团队协作至关重要。
  4. 内置的跨平台抽象 :GN原生理解操作系统(is_linux, is_win, is_mac)和CPU架构(current_cpu)等概念,使得编写跨平台构建规则更为简单和直观。
  5. 工具链的明确建模:GN将"工具链"(包含编译器、链接器、编译标志集等)作为构建图的一等公民,能够优雅地处理主机工具链、目标工具链以及交叉编译等复杂场景。

1.1.3 设计哲学

GN的设计深受其在Chromium项目中替代GYP(Generate Your Projects)的经验影响,其哲学可概括为:

  1. 正确性高于灵活性:GN倾向于要求显式声明,而非依赖隐式规则或魔法行为,这减少了构建配置的歧义和错误。
  2. 简单性高于全能性 :GN有意识地将语言特性保持在最小集,例如,它没有复杂的宏系统,而是通过template机制实现复用,这降低了学习成本和维护负担。
  3. 为大型 项目 优化:GN的所有设计决策都优先考虑在拥有数万源文件和复杂依赖图的项目中的扩展性和性能。

1.1.4 历史背景与定位

GN诞生于Google内部,旨在解决Chromium项目使用GYP时遇到的性能瓶颈配置复杂性问题。其直接前身GYP在项目规模膨胀后,生成Visual Studio等IDE项目文件的速度变得非常缓慢,且配置逻辑难以维护。

因此,GN被设计为 " Ninja 文件生成器" ,而非另一个通用的项目文件生成器。它专注于为Ninja这一追求极致构建速度的系统生成最优输入,从而形成了一个高效的组合:GN负责智能、快速的配置生成,Ninja负责极致、并发的 任务 执行

在Google生态中,GN与Bazel 构成了不同层次的构建工具:Bazel是一个更庞大、支持多语言、包含远程缓存等高级特性的完整构建系统;而GN是一个更轻量、更专注、与C++/Ninja技术栈深度绑定的专用生成器。GN是ChromiumFuchsia操作系统等核心项目的官方构建系统。

1.1.5 与其他构建系统的简要对比

为了清晰定位GN,以下是其与常见构建工具的对比:

特性 GN CMake Make Bazel
本质 元构建系统 (生成Ninja文件) 项目文件/构建系统生成器 任务执行器/构建系统 一体化构建与测试系统
主要后端 Ninja (强制且唯一) Ninja, Make, VS, Xcode 等 无(直接执行) 自研或远程执行
语法风格 声明式,类Python,简洁 命令式,自定义脚本语言 基于规则和命令的声明式 声明式,类Python
核心优势 生成速度极快,与Ninja集成无缝,适合超大型C++项目 生态强大,IDE支持好,生成器后端多 极度灵活,通用性强,Unix原生 可重现性强,支持远程缓存与执行,多语言
主要 场景 大型平台项目 (如浏览器、OS) 通用的跨平台C/C++项目 Unix/Linux环境下的传统项目 多语言微服务项目,需要严格依赖管理和大规模构建

总结而言,GN是一个为解决超大型C++ 项目 构建配置性能与复杂度问题而生的、专注于生成 Ninja 文件的、高效的元构建系统。

1.2 核心概念解析

理解GN中的核心概念是掌握其工作原理的关键。这些概念共同构成了GN构建系统的骨架。

1.2.1 目标(Targets)

目标是GN构建系统的基本构建单元。每个目标代表构建过程中的一个产出物或构建步骤,例如一个可执行文件、一个库或一个自定义生成的文件。

常见目标类型:

1.可执行文件(executable): 生成一个可运行的程序。

javascript 复制代码
executable("my_app") {
  sources = [ "main.cc" ]
}

2.静态库(static_library): 生成一个静态库(如.a.lib文件)。

javascript 复制代码
static_library("my_lib") {
  sources = [ "lib.cc" ]
}

3. 共享 库(shared_library): 生成一个动态链接库(如.so.dll文件)。

javascript 复制代码
shared_library("my_shared_lib") {
  sources = [ "shared.cc" ]
}

4.源文件集(source_set): 将多个源文件组合在一起,但不进行链接。这通常用于将一组文件作为一个单元进行编译,并可由多个目标共享。

javascript 复制代码
source_set("my_sources") {
  sources = [ "util.cc", "helper.cc" ]
}

5.组(group): 将多个目标组合在一起,本身不产生输出文件,仅用于聚合依赖。这在组织构建规则时非常有用。

javascript 复制代码
group("all_my_tools") {
  deps = [ ":tool1", ":tool2" ]
}

6.动作(action): 运行一个自定义脚本或命令来生成一个或多个文件。这允许将自定义构建步骤集成到GN构建中。

ini 复制代码
action("generate_version_header") {
  script = "generate_version.py"
  outputs = [ "$target_gen_dir/version.h" ]
}

每个目标都有多种属性,用于定义其行为。常见的属性包括:

  • sources:该目标包含的源文件列表。
  • deps:该目标所依赖的其他目标列表。
  • configs:应用于该目标的配置列表。
  • public_configs:不仅应用于本目标,还会传递给依赖本目标的其他目标。
  • visibility:控制哪些其他目标可以依赖于本目标。

1.2.2 依赖(Dependencies)

依赖定义了目标之间的关系。GN通过依赖关系来确定构建顺序,并确保每个目标在构建时其所有依赖都已构建完成。

依赖类型:

1.直接依赖(deps): 最常用的依赖类型。表示当前目标在链接时需要这些依赖目标。GN会确保依赖目标先于当前目标构建,并将它们的输出和配置适当传递。

ini 复制代码
# app依赖于my_lib,my_lib会提前构建好
executable("app") {
  ...
  deps = [ ":my_lib" ]
}

2.公共依赖(public_deps): 与普通依赖类似,但还会将依赖目标的public_configslibs等传递给当前目标的依赖者。用于解决依赖链中的配置传递问题。

css 复制代码
static_library("a") {
  ...
  public_deps = [ ":b" ]
}
# 如果目标c依赖a,那么c也会间接获得b的public_configs。

3.数据依赖(data_deps): 表示运行时依赖,但不影响链接。例如,如果程序在运行时需要一些数据文件或脚本,可以使用data_deps来确保它们被构建并复制到正确的位置。

ini 复制代码
# my_program 运行时需要使用data_files,调用helper
# gn会在构建 my_program 之前完成对 data_files 和 helper 的构建
executable("my_program") {
  data_deps = [ ":data_files", ":helper" ]
}

GN会解析所有目标的依赖关系,形成一个有向无环图(DAG),确保没有循环依赖。这个图决定了构建的顺序,并确保每个目标只构建一次。

1.2.3 配置(Configs)

配置是一组编译器标志、预处理器定义、包含目录等设置的集合。配置可以附加到目标上,从而影响目标的构建方式。

定义和使用配置:

使用config语句来定义一个配置:

ini 复制代码
config("my_config") {
  defines = [ "ENABLE_FEATURE=1" ]
  cflags = [ "-Wall" ]
  include_dirs = [ "../include" ]
}

在目标中通过configs属性来应用这个配置:

ini 复制代码
executable("my_app") {
  sources = [ "main.cc" ]
  configs = [ ":my_config" ]
}

目标可以应用多个配置,这些配置会按顺序合并。如果多个配置定义了相同的设置,后应用的配置会覆盖先前的。

公共配置(public_configs):

有时候,一个库不仅需要自己使用某些配置,还需要将这些配置传递给依赖它的目标。这时可以使用public_configs属性。

ini 复制代码
static_library("my_lib") {
  sources = [ "lib.cc" ]
  # 所有依赖"my_lib"的目标都会自动应用"my_public_config"
  public_configs = [ ":my_public_config" ]
}

1.2.4 工具链(Toolchains)

工具链定义了构建目标时使用的编译器、链接器以及其他工具和全局设置。工具链是GN中一个核心的抽象概念,它允许在同一个构建中为不同的平台或架构使用不同的工具(cross-compile)。

工具链组成: 一个工具链通常包括

  • 编译器(例如,cc用于C,cxx用于C++)
  • 链接器(link
  • 其他工具(例如,alink用于静态库,solink用于共享库,stampcopy等)
  • 工具链特定的构建标志和设置

默认工具链和辅助工具链:

每个GN构建都有一个默认工具链,定义在BUILDCONFIG.gn文件中。默认工具链通常用于构建目标平台(target_os)的代码。

此外,还可以定义辅助工具链,例如用于构建主机工具(host_os)的工具链,或者用于交叉编译的工具链。

1.2.5 构建参数(Build args)

构建参数是用户可以在构建时配置的变量,它们允许用户自定义构建的行为,而无需修改构建文件本身。

声明参数:

ini 复制代码
#使用declare_args函数来声明构建参数:
declare_args() {
  enable_logging = true
  optimization_level = 2
}

使用参数:

在构建文件中,可以像使用普通变量一样使用这些参数:

scss 复制代码
config("my_config") {
  if (enable_logging) {
    defines = [ "ENABLE_LOGGING" ]
  }
  cflags = [ "-O$optimization_level" ]
}

传递参数:

在命令行中使用 --args 来设置参数,也可以通过gn args命令交互式地编辑参数:

csharp 复制代码
# 设置构建参数
gn gen out/Default --args='enable_logging=false optimization_level="size"'

# 查看所有参数及其值
gn args out/Default --list

# 交互式编辑参数
gn args out/Default
# 这会打开编辑器,显示当前参数:
# enable_logging = false
# optimization_level = "size"
# ...

# 参数可以引用其他参数
gn gen out/Release --args='is_debug=false is_official_build=true'

参数的 作用域

构建参数在声明的作用域内有效,并向下传递到子目录。通常,参数在项目根目录的BUILD.gnBUILDCONFIG.gn中声明,以便在整个项目中使用。

1.2.6 总结

  • 构建参数控制构建的全局行为
  • 工具链提供构建的执行环境
  • 配置定义具体的编译选项
  • 目标是具体的构建任务
  • 依赖连接目标,形成构建图

1.3 GN工作流程

GN遵循明确的"生成-执行"两阶段工作流程:

1.3.1 GN生成(配置与生成)

此阶段由 gn gen 命令触发,GN 作为元构建系统执行以下工作:

  1. 加载根配置:

    1. 查找并读取源码根目录下的 .gn 文件,确定主构建配置文件(BUILDCONFIG.gn)的位置和项目结构。
  2. 解析构建图:

    1. 递归读取各目录下的 BUILD.gn 文件。
    2. 解析其中定义的所有目标(target)、依赖(deps)和配置(configs)。
    3. 结合命令行传入的 --args 参数,计算所有变量的最终值。
  3. 生成Ninja文件:

    1. 将解析得到的完整构建图(一个包含目标、依赖关系、精确命令的有向无环图)转换为高度优化的 build.ninja 文件。
    2. 该文件包含了构建最终产物所需的所有编译、链接命令及其精确的依赖关系。

产出:out/build.ninja 文件(out 为构建目录示例)。 此阶段完成后,完整的构建计划就已确定,不会因环境变化而改变。

1.3.2 Ninja执行(编译与链接)

此阶段由 ninja -C out 命令触发,Ninja 作为构建执行器工作:

  1. 加载构建计划:

    1. 读取 build.ninja 文件,获知所有需要执行的任务(如编译 main.cc、链接 app)。
  2. 任务调度与执行:

    1. 根据文件依赖关系(如 .o 文件依赖于 .cc.h 文件)确定构建顺序。
    2. 以最大化并行的方式调度任务,调用底层工具链(如 gccclanglink)执行具体的编译、链接命令。
  3. 增量构建:

    1. Ninja 会检查所有输入文件(源码、头文件)的时间戳和哈希值。
    2. 仅重新构建那些输入发生改变或其依赖发生改变的目标,这是构建速度极快的关键。

产出:最终的可执行文件、静态库、动态库等二进制产物。

1.4 文件系统结构

GN构建系统依赖于几种特定类型的配置文件,它们以清晰的层次结构组织在一起,共同定义了整个项目的构建规则:

文件/目录 必需性 典型位置 主要作用 示例
.gn 必需 源码根目录 定义项目根目录和主构建配置 设置 buildconfig 路径
BUILDCONFIG.gn 必需 .gn 指定 全局构建配置、默认工具链声明 定义默认构建变量
BUILD.gn 按需 各源码子目录 定义当前目录的构建目标 声明可执行文件、库
.gni (GN Import) 按需 任意位置 可复用的模板和配置 共享的编译器配置
toolchain/*.gn 通常必需 工具链目录 定义不同平台的工具链 gcc_toolchain.gn

1.4.1 根配置文件 (.gn)

每个GN项目必须在源码根目录包含一个.gn文件,这是GN查找项目根目录的标志。

ini 复制代码
# 示例:简单的 .gn 文件
buildconfig = "//build/config/BUILDCONFIG.gn"  # 主构建配置文件路径
root = "."  # 源码根目录,通常就是当前目录

# 可选:定义次级源码根目录(用于包含第三方代码)
secondary_source = "//third_party"

# 可选:构建参数默认值
default_args = {
  is_debug = true
  target_cpu = "x64"
}

说明

  • GN会从当前目录向上搜索.gn文件,找到的第一个.gn文件所在目录即为源码 根目录
  • buildconfig 必须指向有效的 BUILDCONFIG.gn 文件

1.4.2 主构建配置 (BUILDCONFIG.gn)

这是GN构建系统的核心配置文件,定义了全局的构建环境和默认值。

ini 复制代码
# 示例:简化的 BUILDCONFIG.gn

# 1. 设置默认工具链
if (target_os == "linux") {
  default_toolchain = "//build/toolchain/linux:gcc"
} else if (target_os == "win") {
  default_toolchain = "//build/toolchain/win:msvc"
}

# 2. 定义全局变量
is_debug = true  # 默认调试模式
is_official_build = false  # 非官方构建

# 3. 设置目标平台
target_cpu = "x64"
target_os = "linux"

# 4. 包含通用配置
import("//build/config/sanitizers/sanitizers.gni")
import("//build/config/compiler/compiler.gni")

# 5. 声明构建参数
declare_args() {
  # 用户可以覆盖的默认值
  is_component_build = false
  use_custom_allocator = true
}

说明

  • 这是第一个被GN处理的构建文件
  • 设置影响整个构建过程的全局变量
  • 声明默认工具链
  • 通常由项目维护者编写,普通用户不直接修改

1.4.3 构建定义文件 (BUILD.gn)

每个包含需要构建的源代码的目录通常都包含一个BUILD.gn文件,定义了该目录下的构建目标。

bash 复制代码
# 典型项目结构
my_project/
├── .gn                    # 根配置
├── build/
│   ├── config/
│   │   └── BUILDCONFIG.gn # 主构建配置
│   └── toolchain/
│       ├── linux/
│       │   └── BUILD.gn   # Linux工具链定义
│       └── win/
│           └── BUILD.gn   # Windows工具链定义
├── src/
│   ├── core/
│   │   ├── BUILD.gn       # 核心库定义
│   │   └── *.cc
│   ├── app/
│   │   ├── BUILD.gn       # 应用定义
│   │   └── *.cc
│   └── utils/
│       ├── BUILD.gn       # 工具函数定义
│       └── *.cc
└── third_party/
    ├── zlib/
    │   └── BUILD.gn       # 第三方库适配
    └── json/
        └── BUILD.gn

BUILD.gn 示例

ini 复制代码
# src/core/BUILD.gn

# 静态库定义
static_library("core") {
  sources = [
    "logging.cc",
    "file_util.cc",
    "string_util.cc",
  ]
  
  # 公开的头文件目录
  public = [ "include" ]
  
  # 依赖
  deps = [
    "//third_party/zlib",
  ]
  
  # 配置
  configs = [
    "//build/config:compiler_defaults",
  ]
  
  # 条件编译
  if (is_win) {
    sources += [ "win_specific.cc" ]
    defines = [ "WINDOWS_SPECIFIC" ]
  }
}

# 测试可执行文件
executable("core_tests") {
  testonly = true  # 标记为测试目标
  sources = [
    "logging_unittest.cc",
    "file_util_unittest.cc",
  ]
  
  deps = [
    ":core",
    "//third_party/googletest:gtest_main",
  ]
}

1.4.4 导入文件 (.gni)

.gni文件用于定义可复用的模板、配置和函数,通过import()语句引入。

scss 复制代码
# build/config/compiler.gni

# 定义可复用的编译器配置模板
template("compiler_config") {
  config(target_name) {
    forward_variables_from(invoker, "*")
    
    # 公共编译器标志
    cflags = []
    cflags_cc = []
    
    if (is_clang) {
      cflags += [ "-fcolor-diagnostics" ]
    }
    
    if (is_debug) {
      cflags += [ "-O0", "-g" ]
    } else {
      cflags += [ "-O2", "-DNDEBUG" ]
    }
  }
}

# 在 BUILD.gn 中使用
import("//build/config/compiler.gni")

compiler_config("my_compiler_config") {
  # 可以添加自定义标志
  cflags += [ "-Wall" ]
}

1.4.5 工具链文件

工具链定义通常组织在专门的目录中,每个平台可能有自己的工具链文件。

bash 复制代码
build/toolchain/
├── BUILD.gn              # 工具链聚合文件
├── linux/
│   └── BUILD.gn          # Linux具体配置
├── win/
│   └── BUILD.gn          # Windows具体配置
└── mac/
    └── BUILD.gn          # macOS具体配置

1.4.6 构建输出目录结构

当运行gn gen out时,GN会创建构建输出目录,其中包含生成的文件:

csharp 复制代码
out/
├── build.ninja                    # 生成的Ninja构建文件
├── toolchain.ninja               # 工具链特定的Ninja规则
├── args.gn                       # 保存的构建参数
├── obj/                          # 中间对象文件目录
│   ├── src/
│   │   └── core/
│   │       ├── core.logging.o
│   │       └── core.file_util.o
│   └── ...
├── gen/                          # 生成的源代码目录
│   └── ...
└── my_app                        # 最终输出

说明

  • obj/:编译中间文件(.o.obj
  • gen/:构建过程中生成的头文件、源文件
  • 根目录:最终的可执行文件、库文件

1.4.7 路径标识

  • // 开头的路径从源码根目录开始
  • : 用于分隔目录和目标名(//src/core:core
  • 相对路径相对于当前BUILD.gn文件

2.Hello World 示例

本章将通过一个完整的可运行实例,详细介绍GN构建系统的基本使用流程。我们将创建一个简单的C++项目,演示如何配置GN文件并构建出可执行程序。

2.1 项目结构

项目整体结构如下所示,hello_world.cc 定义了 main() 函数,通过调用 printer.cc 中的接口打印输出 "Hello World!"

bash 复制代码
gn_hello_world/
├── .gn                          # 根配置文件
├── BUILD.gn                     # 根构建定义文件
├── app/                         # 应用程序目录
│   ├── BUILD.gn                # 应用构建定义
│   └── hello_world.cc          # 主程序源码
├── build/                       # 构建配置目录
│   ├── BUILDCONFIG.gn          # 主构建配置
│   └── toolchain/
│       └── BUILD.gn            # 工具链定义
└── util/                       # 工具库目录
    ├── BUILD.gn               # 工具库构建定义
    ├── printer.cc             # 工具库实现
    └── printer.h              # 工具库头文件

2.2 文件内容解析

接下来按照自下而上的顺序,详细介绍项目中各文件的内容和组织关系。

2.2.1 源码及构建定义文件 (BUILD.gn)

2.2.1.1 应用程序源码

//app/hello_world.cc 中定义程序的 main() 入口:

arduino 复制代码
#include "util/printer.h"

int main() {
    // 调用工具库printer中定义的打印函数
    PrintHelloWorld();
    return 0;
}

//app/BUILD.gn中定义可执行文件目标:

ini 复制代码
# 定义可执行目标
executable("hello_world") {
    # 指定构建所需的源文件
    sources = [
        "hello_world.cc",
    ]
    
    # hello_world.cc 中调用了 //util/printer.h 中声明的方法
    # 通过 deps 声明依赖,确保正确链接
    deps = [
        "//util:printer",
    ]
}
2.2.1.2 工具库源码

//util/printer.hprinter.cc 中声明并定义 PrintHelloWorld() 方法:

csharp 复制代码
#pragma once

// Print hello world msg.
extern void PrintHelloWorld();
c 复制代码
#include <iostream>

void PrintHelloWorld() {
    std::cout << "Hello, World!" << std::endl;
}

//util/BUILD.gn中中定义工具库:

ini 复制代码
# 定义源代码集(不进行链接,仅编译)
source_set("printer") {
    # 添加要编译的源文件
    # 注意:头文件也需要列出,GN 会检查 #include 的正确性
    sources = [
        "printer.h",
        "printer.cc",
    ]
    
    # 声明目标对外暴露的头文件
    # 这允许依赖此目标的其他目标正确包含头文件
    public = [
        "printer.h",
    ]
}
2.2.1.3 根构建定义

//app/BUILD.gn 定义了可执行文件 hello_world,但要让构建系统知晓需要构建此目标,还需在根目录的 BUILD.gn 中进行声明:

csharp 复制代码
# 使用 group 目标来指明构建该项目时需要构建 hello_world
# GN 会自动到 //app 目录下的 BUILD.gn 中查找同名目标
group("all") {
    deps = [
        "//app:hello_world",
    ]
}

2.2.2 根配置与工作链

2.2.2.1 根配置文件 (.gn)

准备好源码和构建定义文件后,需要在项目根目录添加 .gn 文件(又称 dotfile):

ini 复制代码
# dotfile 定义项目全局配置
# 详细可配置内容可通过执行 `gn help dotfile` 查看
# 其中,buildconfig 是唯一必须配置的内容
buildconfig = "//build/BUILDCONFIG.gn"

.gn 文件告诉 GN 两件事:

  1. 当前目录是项目源码根目录
  2. 主构建配置文件的位置
2.2.2.2 主构建配置 (BUILDCONFIG.gn)

//build/BUILDCONFIG.gn 中定义项目的全局构建环境和默认值:

ini 复制代码
# 指定项目构建时使用的默认工具链
set_default_toolchain("//build/toolchain:gcc")

# 全局编译器标志
# 作用:所有C++代码编译时将自动应用这些参数,无需在每个BUILD.gn中重复定义
cflags_cc = [
    "-std=c++11",   # 使用 C++11 标准
    "-Wall",        # 开启所有编译器警告
]

# 全局头文件搜索路径
# 将项目根目录加入编译器的头文件搜索路径
# 代码中可使用以项目根目录为起点的绝对路径引入头文件
include_dirs = ["//"]
2.2.2.3 工具链定义

//build/toolchain/BUILD.gn 中定义 GCC 工具链(基于 Chromium 的 GCC 工具链模板简化,copy from: gitee.com/micdav/gene...

ini 复制代码
# 定义一个名为 "gcc" 的工具链目标
toolchain("gcc") {
    # tool("xxx") { ... }:
    #     在工具链内部定义具体的构建工具动作,xxx 是 GN 内置的抽象动作名
    #     GN 对构建动作做了标准化抽象,比如 cc 代表编译 C、cxx 代表编译 C++
    #     开发者只需为这些抽象动作绑定具体的系统命令即可
    #
    # 模板变量 {{xxx}}:
    #     GN 内置的动态替换变量,构建时 GN 会根据项目配置自动替换为实际值
    #     例如 {{source}} 会替换为当前要编译的源文件路径
    
    # C 语言编译工具定义
    tool("cc") {
        depfile = "{{output}}.d"
        command = "gcc -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_c}} -c {{source}} -o {{output}}"
        depsformat = "gcc"
        description = "CC {{output}}"
        outputs = [
            "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
        ]
    }
    
    # C++ 语言编译工具定义
    tool("cxx") {
        depfile = "{{output}}.d"
        command = "g++ -MMD -MF $depfile {{defines}} {{include_dirs}} {{cflags}} {{cflags_cc}} -c {{source}} -o {{output}}"
        depsformat = "gcc"
        description = "CXX {{output}}"
        outputs = [
            "{{source_out_dir}}/{{target_output_name}}.{{source_name_part}}.o",
        ]
    }
    
    # 静态库链接工具定义
    tool("alink") {
        rspfile = "{{output}}.rsp"
        command = "rm -f {{output}} && ar rcs {{output}} @$rspfile"
        description = "AR {{target_output_name}}{{output_extension}}"
        rspfile_content = "{{inputs}}"
        outputs = [
            "{{target_out_dir}}/{{target_output_name}}{{output_extension}}",
        ]
        default_output_extension = ".a"
        output_prefix = "lib"
    }
    
    # 动态库链接工具定义
    tool("solink") {
        soname = "{{target_output_name}}{{output_extension}}"  # 例如 "libfoo.so"
        sofile = "{{output_dir}}/$soname"
        rspfile = soname + ".rsp"
        
        command = "g++ -shared {{ldflags}} -o $sofile -Wl,-soname=$soname @$rspfile"
        rspfile_content = "-Wl,--whole-archive {{inputs}} {{solibs}} -Wl,--no-whole-archive {{libs}}"
        
        description = "SOLINK $soname"
        default_output_extension = ".so"
        default_output_dir = "{{root_out_dir}}"
        
        outputs = [
            sofile,
        ]
        link_output = sofile
        depend_output = sofile
        output_prefix = "lib"
    }
    
    # 可执行文件链接工具定义
    tool("link") {
        outfile = "{{target_output_name}}{{output_extension}}"
        rspfile = "$outfile.rsp"
        command = "g++ {{ldflags}} -o $outfile -Wl,--start-group @$rspfile {{solibs}} -Wl,--end-group {{libs}}"
        description = "LINK $outfile"
        default_output_dir = "{{root_out_dir}}"
        rspfile_content = "{{inputs}}"
        outputs = [
            outfile,
        ]
    }
    
    # 占位文件生成工具(用于依赖管理)
    tool("stamp") {
        command = "touch {{output}}"
        description = "STAMP {{output}}"
    }
    
    # 文件复制工具
    tool("copy") {
        command = "cp -af {{source}} {{output}}"
        description = "COPY {{source}} {{output}}"
    }
}

这个工具链定义涵盖了C/C++项目构建所需的基本工具,为GN提供了从源码到可执行文件的完整转换规则。

2.3 编译执行

1.配置生成

在项目根目录下执行以下命令:

csharp 复制代码
gn gen out

该命令会在 //out 目录下生成构建所需的 Ninja 文件。

2.编译与链接:

继续执行以下命令:

csharp 复制代码
ninja -C out

该命令会:

  1. 进入 out 目录
  2. 读取生成的 build.ninja 文件
  3. 按照依赖顺序编译所有源文件
  4. 链接生成最终的可执行文件

3.运行程序:

编译完成后,在 //out 目录下会生成 hello_world 可执行文件,执行输出 "Hello World!":

3.详细说明

本章将深入探讨GN构建系统的各个方面,从基础语法到高级特性。

本章的内容可通过执行 gn help xxx 来查看gn官方的详细解释。

3.1 语法详解

GN采用了一种极简、动态类型的命令式语言,其核心目的是生成声明性的Ninja构建规则。其语法设计严格遵循明确性高于灵活性的原则,以消除构建配置中的歧义。

3.1.1 基本语法规则

标识符:

  • 由字母、数字和下划线组成,区分大小写。
  • 不能以数字开头。

注释:

  • 仅支持单行注释,以 # 开头,延续至行尾。

数据类型:

GN支持以下基本类型:

类型 描述 示例
布尔值 true 或 false is_debug = true
64位有符号整数 十进制表示,不支持浮点数 count = 42
字符串 必须使用双引号包括,支持简单的变量替换 name = "hello"
列表 可包含任意类型的元素,使用方括号 \[\] 定义 list = 1, "foo", true
作用域 使用花括号 {} 定义的键值对集合,是GN中组织数据的主要方式 scope = { key = "value" }

字符串:

  • 引号:必须使用双引号 (") 定义。
  • 转义:仅支持 "(双引号)、$(美元符号)和 \(反斜杠)三种转义序列。其他反斜杠字符(如 \n, \t)均被视为字面量,这使得Windows路径(如 "C:\foo\bar.h")无需转义。
  • 变量替换:字符串内支持使用 $var${var} 的形式进行立即变量替换.
ini 复制代码
path = "src"
file = "$path/foo.cc"       # 结果为 "src/foo.cc"
file2 = "${path}/foo.cc"    # 结果为 "src/foo.cc"

列表:

  • 创建:使用方括号 [],元素以逗号分隔。
  • 拼接与删除:支持使用 +- 进行列表的拼接与删除操作。
  • 追加与删减:支持使用 +=-= 进行原地修改。
  • 索引访问:支持使用 list[0] 语法进行只读访问,但不能通过索引修改列表。
  • 赋值限制:禁止将非空列表直接赋值给已包含非空列表的变量。必须先赋值为空列表 [] 进行清除
ini 复制代码
a = [1, 2]
a += [3]          # 正确:追加,a 变为 [1, 2, 3]
b = a + [4]       # 正确:拼接,b 为 [1, 2, 3, 4]
c = a - [2]       # 正确:删除,c 为 [1, 3]

d = [5, 6]
d = [7, 8]        # 错误:尝试用非空列表覆盖非空列表
d = []            # 正确:先清空
d = [7, 8]        # 正确

3.1.2 变量与作用域

变量赋值:

  • 赋值 (=): 在当前作用域创建或覆盖一个变量。
  • 追加 (+=): 仅适用于列表,将右侧列表的元素追加到左侧变量中。对字符串或其他类型使用 += 会导致错误。

作用域 规则:

  • 嵌套查找:读取变量时,GN从最内层作用域开始向外逐层查找。
  • 写入隔离:变量赋值始终发生在当前最内层作用域,无法直接修改外层作用域的变量。
  • 块级作用域:ifelseforeach 等控制语句不会创建新的作用域,在其中对变量的修改会影响外部。新的作用域通常由函数调用、目标定义创建。
ini 复制代码
outer_var = "outer"
source_set("test"){
    # 可以访问外层变量
    inner_var = outer_var  # "outer"
    # 此赋值创建的是当前块作用域内的新变量,不会影响外层的 outer_var
    outer_var = "shadowed"
}
# 此处 outer_var 的值仍为 "outer"

3.1.3 控制结构

条件语句(if / else if / else):

  • 语法类似C语言,条件必须用括号括起。
  • 不支持三元运算符 ? :
ini 复制代码
if (is_linux) {
    defines = ["OS_LINUX"]
} else if (is_win) {
    defines = ["OS_WIN"]
} else {
    defines = ["OS_UNKNOWN"]
}

循环( foreach ):

  • 仅支持 foreach 循环,用于迭代列表。
  • 循环变量是元素的副本,修改它不会影响原列表。
  • 官方建议谨慎使用,因为大多数构建逻辑应无需循环即可表达。
scss 复制代码
files = ["a.cc", "b.cc"]
foreach(file, files) {
    print("Processing: " + file)  # file 是列表元素的副本
}

断言(assert):

  • 用于在配置阶段强制满足特定条件,条件不满足则构建失败。
ini 复制代码
assert(target_cpu == "x64" || target_cpu == "arm64",
       "Only x64 and arm64 are supported")

调试输出(print):

  • 在生成构建文件时输出信息,仅用于调试。
bash 复制代码
print("Current OS is: " + current_os)

3.2 目标类型深度解析

在GN中,目标(Target)是构建的基本单元和核心概念 。它代表一个构建步骤的产出,例如一个可执行文件、一个静态库、一组源文件,甚至是一个自定义的脚本动作。每个目标都定义在 BUILD.gn 文件中,通过特定的函数调用来声明,并包含一组描述其如何构建的属性。

3.2.1 主要目标类型

GN提供了多种目标类型,以适应不同的构建需求。最常见和基础的类型如下:

目标类型 声明函数 描述与产出
可执行文件 executable 构建一个独立的、可直接运行的程序。例如:my_app (Linux) 或 my_app.exe (Windows)。
静态库 static_library 构建一个在编译时被链接(归档)到其他目标中的库。例如:libbase.a (Linux) 或 base.lib (Windows)。
共享 shared_library 构建一个在运行时被动态加载的库(如DLL或SO)。例如:libui.so (Linux) 或 ui.dll (Windows)。
源代码 source_set 不产生链接库。它将一组源文件编译成对象文件,并允许其他目标以非常高效的方式复用这些对象文件,避免重复编译。是GN中组织代码的常用方式。
group 不产生任何输出文件 。它是一个纯粹的"元目标",用于将多个依赖聚合在一起。依赖一个group目标等同于直接依赖其deps列表中的所有目标。常用于组织构建入口(如//:all)。
动作 action 运行一个自定义脚本或命令,用于生成文件(如代码、资源、文档)。其输出可作为其他目标的输入。

其他实用类型

  • copy:将文件复制到构建目录。
  • action_foreach:为输入文件列表中的每个文件运行一次脚本。
  • loadable_module:类似于共享库,但不参与主程序的链接(如插件)。

Note :要了解所有目标类型及其属性的详细信息,请查阅官方 reference.md 文档,或在命令行中使用 gn help <target_type> 命令,如 gn help executable

3.2.2 目标属性详解

目标的构建行为由其属性(键值对)控制。以下是最关键的几个属性:

  • sources (列表)

定义构成该目标的源文件列表。GN会根据文件后缀自动识别并使用正确的编译工具(.cc, .cpp 使用 C++ 编译器)。

  • deps (依赖列表)

定义此目标所依赖的其他目标 。GN确保在构建当前目标之前,所有在 deps 中列出的目标都已构建完成,并且它们的输出(如库)可用于链接。

ini 复制代码
deps = [
  "//base",           # 依赖另一个目录下的默认目标
  "//net:http",       # 依赖 `//net` 目录下名为 `http` 的目标
  ":my_helper_lib",   # 依赖同一 `BUILD.gn` 文件内名为 `my_helper_lib` 的目标
]
  • public_deps (传递依赖列表)

普通 deps 的依赖关系是私有的。而 public_deps 意味着依赖关系可以传递 。当目标 A 通过 public_deps 依赖 B 时,任何依赖 A 的目标也将自动继承对 B 的依赖。这在库暴露其接口所依赖的另一个库时至关重要。

ini 复制代码
# //ui/BUILD.gn
shared_library("ui") {
  public_deps = [ "//skia" ] # ui 公开依赖 skia
}

# //app/BUILD.gn
executable("my_app") {
  deps = [ "//ui" ] # 只需声明依赖 ui,自动获得对 skia 的链接
  # 无需(也不应)再写 deps = [ "//skia" ]
}
  • configspublic_configs (配置列表)

configs 定义应用于当前目标的编译设置(如defines, cflags)。

public_configs 用于指定那些不仅应用于当前目标,还要传递给所有依赖此目标的其他目标 的配置(通常是头文件搜索路径 include_dirs 和公开的宏定义 defines)。

  • visibility (可见性列表)

控制哪些其他目标可以依赖此目标。这是一个重要的项目管理工具。

ini 复制代码
visibility = [
  ":*", # 仅允许同一 `BUILD.gn` 文件内的目标依赖我
  "//my_project/*", # 允许 `my_project` 目录下的所有目标依赖我
  "//*", # 允许项目内所有目标依赖我(公开)
]

如果未指定 visibility,则目标默认仅对所有目标可见。

3.2.3 目标定义示例与 最佳实践

一个完整的目标定义示例如下:

ini 复制代码
# 在 //foo/BUILD.gn 中

# 1. 定义一个静态库
static_library("foo") {
  # 输出名称可选,默认为目标名"foo"
  # output_name = "foo_core"

  sources = [
    "foo.cc",
    "foo_utils.cc",
  ]

  # 公开此库的头文件目录,允许依赖者包含
  public = [ "include" ]
  public_configs = [ ":foo_config" ]

  # 内部依赖,不暴露给使用者
  deps = [
    "//third_party:zlib",
  ]

  # 仅允许同目录和测试目标依赖此库
  visibility = [
    ":*",
    "//foo/tests:*",
  ]
}

# 2. 为该库定义一个配置
config("foo_config") {
  include_dirs = [ "include" ]
  defines = [ "FOO_IMPLEMENTATION" ]
}

# 3. 定义一个使用该库的可执行文件
executable("foo_tool") {
  sources = [ "tool.cc" ]
  deps = [
    ":foo", # 依赖我们上面定义的库
  ]
}

最佳实践 建议

  1. 命名一致 :目标通常与其所在的目录同名(例如 //base 目录下的 base 库),或在目录内具有描述性的名称
  2. 善用 source_set :对于不打算作为独立库发布的大量内部工具代码,优先使用 source_set 来提升构建速度。

3.3 依赖管理

依赖管理是GN构建系统的核心。它通过明确声明目标间的依赖关系,构建出一个精确的有向无环图,Ninja据此确定正确的构建顺序。GN提供了不同种类的依赖以满足模块化、封装和跨工具链构建的需求。

3.3.1 依赖的类型与区别

GN主要定义了三种依赖关系,它们在传递性、链接行为和适用场景上区别如下:

属性 类型 传递性 主要作用 典型场景
deps 私有依赖 链接与构建顺序:确保当前目标能链接到依赖目标的输出(如库文件),并在其之后构建。 依赖一个实现库,但不想暴露该库给下游使用者。
public_deps 公开依赖 接口与配置传递 :除了具备deps的功能外,还将自身的公开依赖public_configs 传递给依赖它的目标。 当一个库(A)在其公开头文件 中包含了另一个库(B)的头文件时,A必须将B声明为public_deps,这样依赖A的用户才能正确找到B的头文件。
data_deps 数据依赖 运行时文件保证 :确保依赖目标在当前目标运行前 已被构建,但不进行链接。不保证构建顺序(可能并行构建)。 依赖一个在运行时需要被加载的资源文件、脚本或独立工具。

关键概念: public 头文件列表

一个目标可以通过public变量明确声明哪些头文件是其公开接口。只有列在public中的头文件才能被依赖此目标的其他目标#include。如果未定义public,则sources中所有的头文件都被视为公开的。

3.3.2 依赖的解析与使用

私有依赖 ( deps )

私有依赖是最常见的形式,用于声明实现上的依赖。

ini 复制代码
# //base/BUILD.gn
source_set("base") {
  sources = [ "log.cc" ]
  public = [ "log.h" ] # 声明log.h为公开头文件
}

# //network/BUILD.gn
source_set("network") {
  sources = [ "socket.cc" ]
  deps = [ "//base" ] # 网络模块私有依赖于基础模块
  # network可以#include "base/log.h",但不会传递此依赖
}
公开依赖 ( public_deps )

公开依赖用于传递接口依赖。当目标A在其公开API中使用了目标B的定义时,必须使用public_deps

ini 复制代码
# //ui/BUILD.gn
shared_library("ui") {
  sources = [ "window.cc" ]
  public = [ "window.h" ]
  # ui库的公开头文件window.h中包含了"//skia/canvas.h"
  public_deps = [ "//skia" ] # 公开依赖skia库
}

# //app/BUILD.gn
executable("my_app") {
  sources = [ "main.cc" ]
  deps = [ "//ui" ] # 应用依赖ui库
  # 由于//ui使用了public_deps,my_app可以间接访问//skia的公开头文件
  # 并且链接时也会自动链接//skia库
}

在上例中,如果//ui错误地使用了deps来依赖//skia,那么gn check会报错,因为my_app无法找到//skia/canvas.h

数据依赖 ( data_deps )

数据依赖用于保证资源在运行时可用。

ini 复制代码
# //tools/BUILD.gn
executable("code_generator") { ... }

# //app/BUILD.gn
action("generate_resources") {
  script = "//scripts/generate.py"
  inputs = [ ... ]
  outputs = [ "$target_gen_dir/resources.bin" ]
  # 生成脚本需要调用code_generator工具
  data_deps = [ "//tools:code_generator" ]
}

3.3.3 可见性 ( visibility ) 控制

可见性(visibility)是一种访问控制机制,用于限制哪些目标可以依赖当前目标,它独立于依赖类型,并在每次GN命令执行时检查。这对于维护清晰的模块边界至关重要。

scss 复制代码
# //core/BUILD.gn
static_library("internal_core") {
  # 只有当前目录下的目标可以依赖我
  visibility = [ ":*" ]
}

static_library("public_core") {
  # 允许项目内所有目标依赖我
  visibility = [ "//*" ]
}

static_library("restricted_core") {
  # 允许特定目录和特定目标依赖我
  visibility = [    "//app/*",    "//tests:integration_tests",  ]
}
  • ":*":仅限同一BUILD.gn文件内的目标。
  • "//*":整个项目内的所有目标。
  • "//foo/*"//foo目录下的所有目标。
  • 如果未设置visibility,则默认为公开("//*")。

配置(config)同样可以设置visibility,以限制哪些目标可以应用此配置。在编写模板时,通常只将visibility应用在最终暴露给用户的目标上,以确保内部辅助目标之间可以正常依赖。

3.3.4 依赖检查与工具 ( gn check )

GN提供了gn check命令来验证源代码中的#include语句是否与构建文件中声明的依赖关系匹配。其核心规则是: #include 目标Y的头文件,目标X必须在其依赖链中能通过直接的 deps 或连续的 public_deps 关系到达Y

运行检查:

sql 复制代码
# 对现有构建目录进行检查
gn check out/Default
# 在生成构建文件时同时进行检查(如CI环境)
gn gen out/Default --check

自动化工具如fix-deps可以分析gn check的错误,并自动为BUILD.gn文件添加缺失的依赖。其基本逻辑是:如果一个公开头文件 包含了另一个目标的头文件,则添加public_deps;如果是实现文件(.cc或私有头文件)包含,则添加deps

3.3.5 综合规则

综上所述,在GN中,目标X要能#include目标Y的头文件,必须同时满足以下条件:

  1. 可见性 :X在Y的visibility列表内(或Y未设置visibility)。
  2. 头文件公开 :该头文件在Y的public列表内(或Y未设置public变量)。
  3. 依赖连通性 :X直接依赖于Y,或存在一条从X到Y的路径,且该路径全部由public_deps连接。

3.4 配置(config)

配置(Config)是GN构建系统中用于集中管理编译设置(如编译器标志、预处理器定义、头文件搜索路径等)的核心机制。它通过 config() 函数定义,本质上是一个命名的、可重用的 作用域,其中封装了一系列构建参数。合理使用配置能有效提升构建文件的可维护性和一致性。

3.4.1 配置的定义

使用 config() 函数并指定一个名称来定义配置。在配置的作用域内,可以设置与目标相同的各类编译相关属性。

ini 复制代码
# 定义一个名为 "optimization" 的配置
config("optimization") {
  # 编译器标志
  cflags = ["-O2"]
  # C++特定的编译器标志
  cflags_cc = ["-fno-rtti"]
  # 预处理器定义
  defines = ["NDEBUG", "ENABLE_OPTIMIZE=1"]
  # 头文件搜索路径
  include_dirs = ["//third_party/boost/include"]
  # 链接器标志 (通常对库的目标设置更有效)
  ldflags = ["-flto"]
}

配置本身不产生任何输出文件,它只是一个设置的集合,必须被应用到目标上才会生效。

3.4.2 配置的应用

配置通过目标的 configspublic_configs 属性被应用。

  • configs :将配置直接应用于当前目标。这些设置仅影响当前目标自身的编译。
  • public_configs :将配置应用于当前目标,并且传递给所有直接依赖此目标的其他目标 。这是库目标向使用者传递必要编译设置(最典型的是 include_dirs)的标准方式。
ini 复制代码
# //foo/BUILD.gn

# 1. 定义一个库的公开配置,通常包含其公开头文件路径
config("foo_config") {
  # 依赖此库的目标需要能找到这些头文件
  include_dirs = [ "include" ]
  defines = [ "FOO_API_EXPORT" ]
}

# 2. 定义库的内部配置,仅影响库自身的编译
config("foo_internal_config") {
  cflags = [ "-Wno-unused-private-field" ]
}

static_library("foo") {
  sources = [ "src/foo.cc" ]
  # 公开配置:使用者也需要这些设置才能正确编译
  public_configs = [ ":foo_config" ]
  # 私有配置:仅用于编译本库
  configs = [ ":foo_internal_config" ]
  deps = [ "//base" ]
}

# //app/BUILD.gn

executable("my_app") {
  sources = [ "main.cc" ]
  deps = [ "//foo" ] # 依赖 foo 库
  # 无需手动添加 `-I//foo/include` 或 `-DFOO_API_EXPORT`
  # 因为 //foo 通过 public_configs 自动传递了 `:foo_config`
}

3.4.3 配置的 继承 、合并与覆盖

配置可以形成链式结构,一个配置可以继承其他配置。配置列表中的项存在顺序,后面的配置会覆盖前面配置中同名的字符串/列表变量

  • 继承 :在配置内部使用 configs 属性来"继承"其他配置。

  • 合并与覆盖 :在目标的 configspublic_configs 列表中,靠后的配置会修改靠前配置的设置。

    • 字符串:完全覆盖。
    • 列表 :完全替换,不是追加
ini 复制代码
config("base_config") {
  cflags = [ "-Wall" ]
  defines = [ "BASE=1" ]
}

config("extended_config") {
  # 继承 base_config 的所有设置
  configs = [ ":base_config" ]
  # 覆盖 cflags 列表(完全替换,-Wall 会丢失!)
  cflags = [ "-Wextra" ]
  # 追加 defines 的正确方式:使用 +=
  defines = [ "EXTENDED=1" ] # 错误!这会覆盖 [ "BASE=1" ]
  defines += [ "EXTENDED=1" ] # 正确!结果为 [ "BASE=1", "EXTENDED=1" ]
}

executable("example") {
  configs = [
    ":base_config",
    ":extended_config", # 此配置中的 cflags 会覆盖前面的
  ]
}

3.4.4 默认配置链

GN的构建全局配置文件(BUILDCONFIG.gn)会为所有目标设置一套默认配置链 。在目标的 configs 中使用 += 时,是在这个默认链的基础上添加。

可以通过打印 configs 变量来查看某个目标最终应用的所有配置及其顺序:

scss 复制代码
executable("debug_target") {
  # ...
  print("Final configs for this target: " + configs)
}

3.5 工具链

工具链是GN构建系统的执行引擎,它是一组用于编译源代码的命令和构建标志的集合。toolchain()函数用于定义这些命令。工具链是GN多平台构建和交叉编译的关键。

3.5.1 核心概念:单工具链与多工具链

根据 gn help toolchain,工具链的使用分为两种模式:

  • 简单构建(单工具链) :整个项目只使用一个工具链。此时,主构建配置文件(BUILDCONFIG.gn)仅在构建开始时加载一次。它必须 调用 set_default_toolchain() 来指定默认工具链的标签。在此模式下,工具链定义中的 toolchain_args 会被忽略。

  • 多工具链构建 :在一个构建中同时使用多个工具链 ,并且同一个目标可以存在于不同的工具链中

    • 当目标A依赖于使用不同工具链的目标B时,GN会启动一个使用B的工具链的次级构建来解析B。
    • 关键行为 :每一个BUILD.gn文件,在其被引用的每个工具链中都会独立执行一次 。这意味着,GN代码可以根据当前工具链(通过current_toolchain变量判断)来改变目标的任何参数,甚至决定该目标是否存在。

3.5.2 工具链的定义与加载机制

定义工具链:

一个工具链通过 toolchain() 函数定义,主要包括两部分:

  1. 工具( tool :为 cccxxalinklinksolink 等抽象步骤指定具体的命令行。
  2. 参数( toolchain_args :一个作用域,用于覆盖或传递构建参数给该工具链。当此工具链作为"备用工具链"被调用时,这些参数会生效。

加载备用工具链的三步流程:

当GN需要为一个依赖加载备用(次级)工具链时,其过程是确定且有序的,如下图所示:

这个流程保证了每个工具链都在其专属的参数配置下独立运行。

3.5.3 工具链参数 ( toolchain_args ) 与配置流向

toolchain_args 是理解工具链配置双向流的关键。

  • 作用 :它是一个作用域变量,其中的变量名对应 declare_args() 块中声明的变量。当调用备用工具链时,toolchain_args 中指定的值会覆盖通过 gn args 或系统默认提供的构建参数。
  • 忽略规则 :如果正在定义的工具链就是默认工具链 ,那么 toolchain_args 会被忽略,因为默认工具链应使用全局约定的参数值。

3.5.4 实践中的关键变量

变量 说明
current_toolchain 当前工具链的 标签。这是最重要的变量,用于在任何目标或配置中判断当前所处的工具链上下文,并据此做出条件判断。
default_toolchain 默认工具链的 标签 。其值由 BUILDCONFIG.gn 中的 set_default_toolchain() 调用设定。
current_cpu / current_os 源于当前工具链 toolchain_args 的固有属性,用于判断架构与操作系统。编写跨平台规则时应使用它们,而非 target_cpu/target_os

示例:依赖不同工具链的目标

ini 复制代码
# 依赖一个使用特定主机工具链构建的代码生成器
executable("my_app") {
  deps = [
    # 使用带工具链标注的标签
    "//tools:code_generator(//build/toolchain/linux:clang_x64)"
  ]
}

# 在代码生成器的定义中,可以根据工具链决定其内容
if (current_toolchain == "//build/toolchain/linux:clang_x64") {
  executable("code_generator") {
    # 仅当处于主机工具链时才定义此目标
  }
}

3.6 构建参数

构建参数允许使用者在构建时(而非编写构建文件时)对构建过程进行灵活配置,而无需修改BUILD.gn等源码文件。这为实现条件编译、功能开关、路径定制等提供了标准化接口。

3.6.1 声明构建参数: declare_args()

构建参数通过 declare_args() 函数块进行声明。在此块内定义参数并指定其默认值,其语法与常规变量赋值类似。

ini 复制代码
declare_args() {
  # 布尔型参数,通常用于功能开关
  enable_logging = true
  is_component_build = false

  # 字符串型参数,用于路径、版本号等
  target_os = "linux"
  custom_sysroot = ""

  # 列表型参数
  extra_defines = []
  system_include_dirs = [ "/usr/local/include" ]

  # 参数可以引用先前定义的参数
  enable_debug_features = enable_logging && is_debug
}

关键点

  • declare_args() 块外,无法使用未声明的构建参数。
  • 参数可以在块内进行条件判断和计算,基于已定义的参数(包括GN内置的如 is_debug, target_cpu)来决定其他参数的默认值。

3.6.2 参数类型与特性

GN构建参数支持以下主要类型,其行为与GN语言中的对应类型一致:

类型 示例 默认值 特点与用途
布尔值 enable_foo = true false 最常用的类型,用于控制功能模块的编译与否。
字符串 mode = "release" "" (空字符串) 用于指定路径、编译模式、版本等文本信息。
列表 flags = ["-Wall"] [] (空列表) 用于传递一组值,如额外的编译标志、定义等。支持 += 追加。
作用域 config = { a = 1 } {} (空作用域) 用于传递一组结构化的键值对,通常用于复杂配置。较少使用。

3.6.3 作用域 继承 规则

构建参数拥有明确的作用域。

  • 全局 作用域 :在 BUILDCONFIG.gn 或顶级 BUILD.gndeclare_args() 中声明的参数,在整个项目范围内可见。
  • 目录 作用域 :在子目录的 BUILD.gn 文件中,可以再次使用 declare_args()。这会在当前目录及其所有子目录中创建一个新的参数作用域

3.6.4 在构建文件中使用参数

声明后的构建参数可以像普通变量一样,在 BUILD.gn.gni 以及 BUILDCONFIG.gn 中的任何地方使用。

ini 复制代码
# 在配置中使用
config("my_config") {
  if (enable_logging) {
    defines = ["ENABLE_LOGGING"]
  }
  cflags = extra_cflags  # 使用列表参数
}

# 在目标中使用
executable("my_app") {
  sources = [ "main.cc" ]
  if (target_os == "linux") {
    sources += [ "linux_specific.cc" ]
  }
  deps = [
    "//base",
  ]
  if (is_component_build) {
    deps += [ "//sandbox" ]
  }
}

3.6.5 命令行管理与 gn args

构建参数的主要管理接口是命令行。用户可以通过多种方式设置参数,优先级从高到低为:命令行指定 > args.gn 文件 > declare_args() 默认值。

1. 在生成时指定 ( gn gen --args )

ini 复制代码
# 一次性生成并设置参数
gn gen out/Default --args='enable_logging=false target_os="android" extra_defines=["FOO", "BAR"]'

2. 使用 gn args 命令管理

这是最常用的方式,用于查看、设置或持久化修改某个构建目录的参数。

csharp 复制代码
# 交互式编辑参数(会打开默认文本编辑器)
gn args out/Default
# 在打开的编辑器窗口中,可以看到并修改所有参数:
# enable_logging = true
# target_os = "linux"
# extra_defines = ["FOO"]

# 列出当前所有参数及其值
gn args out/Default --list

# 快速设置一个参数
gn args out/Default --set=enable_logging false

执行 gn args 后,参数会永久保存在构建目录下的 args.gn 文件中。后续执行 gn genninja 构建命令时,都会自动读取这些参数。

3. 在工具链中使用 ( toolchain_args )

如前一小节所述,在定义工具链时,可以通过 toolchain_args 作用域为备用工具链指定一套专用的参数集,这在交叉编译时尤为重要。

3.7 模板

模板是GN构建系统中实现构建逻辑复用和抽象的高级特性。它允许开发者定义自定义的构建规则,这些规则在使用时看起来就像是GN语言新增的内置目标类型。

3.7.1 模板的基本概念

定义 :模板通过 template() 函数声明,它定义了一个命名代码块,该代码块可以接收参数并生成一个或多个构建目标。

目的 :模板的主要目的是封装和复用复杂的构建逻辑 。当发现自己在多个 BUILD.gn 文件中重复编写类似的目标和动作时,就应该考虑将其提取为一个模板。

共享 :模板通常被定义在具有 .gni 扩展名的文件中,并通过 import() 语句引入到需要使用它的 BUILD.gn 文件中,从而实现跨项目的共享。

3.7.2 模板的工作机制:闭包与调用者上下文

理解模板的关键在于理解其独特的作用域和变量传递机制。

  1. 闭包 :当 template() 被定义时,它会捕获定义点时当前 作用域 中的所有变量,形成一个闭包。这意味着模板内部可以访问定义它的环境中的变量。

  2. 调用者上下文 ( invoker ) :当模板被调用时(如 my_template("foo") { ... }),调用块 { ... } 内定义的变量会被打包成一个名为 invoker 的隐式作用域变量,并传递给模板的执行代码。

  3. 当前目录 :一个重要的例外是"当前目录"这个概念不会 被闭包捕获。在模板内部执行时,当前目录始终是调用者 BUILD.gn 文件所在的目录。这意味着:

    1. 模板可以安全地直接使用 invoker.sources 等调用者提供的相对路径。
    2. 如果模板需要引用自己附带的脚本文件,必须使用基于 源码 根目录的绝对路径 (如 "//build/my_script.py")。

3.7.3 关键特性与编写模式

1. 变量转发

模板通常需要将调用者提供的参数转发给它内部创建的目标。由于调用者可能没有提供所有参数,安全的做法是使用 defined() 进行检查或使用 forward_variables_from() 辅助函数。

scss 复制代码
template("example_template") {
  # 方式1:手动检查并转发可选变量
  target_deps = []
  if (defined(invoker.deps)) {
    target_deps = invoker.deps
  }

  # 方式2:使用辅助函数(推荐)
  # 转发 invoker 作用域中的 deps 和 public_deps 变量
  forward_variables_from(invoker, ["deps", "public_deps"])
  # 转发 invoker 作用域中所有以 "_flags" 结尾的变量
  forward_variables_from(invoker, "*_flags")
  # 转发 invoker 作用域中的所有变量(慎用)
  forward_variables_from(invoker, "*")

  action(target_name + "_action") {
    # ...
  }
}

2. 目标命名规则

  • 主要目标 :模板应创建一个与调用者指定的 target_name 同名的主要目标(如 source_set(target_name))。这是外部目标依赖的接口。
  • 内部目标 :模板创建的所有其他辅助目标(如执行脚本的 action),其名称必须唯一 。通用做法是使用 "${target_name}_描述性后缀"(如 "${target_name}_code_gen")。

3. 覆盖内置目标

模板可以与内置目标(如 executable, shared_library)同名。当发生同名时,模板的优先级高于内置目标。这允许了开发者扩展或定制内置目标。

scss 复制代码
# 扩展 shared_library,为其自动添加一个版本文件
template("shared_library") {
  # 在模板内部,'shared_library' 仍然指向内置的实现
  shared_library(target_name) {
    # 首先转发调用者的所有参数
    forward_variables_from(invoker, "*")
    # 然后添加模板的通用逻辑
    if (!defined(sources)) {
      sources = []
    }
    sources += [ "$target_gen_dir/version_info.cc" ]
    # ... 可能还有生成 version_info.cc 的 action ...
  }
}

3.7.4 完整示例: IDL 文件编译器

以下示例展示了一个完整的模板,它定义了一个规则:将 .idl 接口定义语言文件编译为 C++ 头文件和源文件。

ini 复制代码
# //build/idl.gni
template("my_idl") {
  # 1. 参数验证:友好地检查必要参数
  assert(defined(invoker.sources),
         "Need sources in $target_name listing the idl files.")

  # 2. 为内部代码生成动作创建唯一名称
  code_gen_target_name = target_name + "_code_gen"

  # 3. 定义代码生成动作(内部目标)
  action_foreach(code_gen_target_name) {
    # 获取调用者提供的 IDL 源文件列表
    sources = invoker.sources
    # 必须使用绝对路径指向模板附带的脚本
    script = "//tools/idl/idl_code_generator.py"
    # 定义输出文件模式,GN会根据每个源文件进行展开
    outputs = [
      "$target_gen_dir/{{source_name_part}}.cc",
      "$target_gen_dir/{{source_name_part}}.h",
    ]
  }

  # 4. 定义主要目标:编译生成代码的源文件集合
  source_set(target_name) {
    # 动态获取代码生成动作的所有输出文件作为本目标的源文件
    sources = get_target_outputs(":$code_gen_target_name")
    # 依赖代码生成动作,确保其先执行
    deps = [ ":$code_gen_target_name" ]
    # 可选:转发调用者可能提供的其他配置,如 public_deps
    forward_variables_from(invoker, ["public_deps", "visibility"])
  }
}

调用上述模板:

scss 复制代码
# //foo/BUILD.gn
import("//build/idl.gni") # 导入模板定义

# 调用模板,效果类似于声明了一个新类型的目标 'my_idl'
my_idl("foo_protos") {
  # 这些变量将成为模板内的 invoker.sources
  sources = [ "foo.idl", "bar.idl" ]
  # 可以传递模板设计支持的额外参数
  visibility = [ ":*" ]
}

# 其他目标可以像依赖普通目标一样依赖模板生成的目标
executable("my_app") {
  deps = [ ":foo_protos" ] # 依赖模板生成的主要目标
  # GN会自动处理底层的依赖链:my_app -> foo_protos(source_set) -> foo_protos_code_gen(action)
}

附录

A. GN命令速查表

csharp 复制代码
# 基础命令
gn gen out/Default           # 生成构建目录
gn args out/Default          # 编辑构建参数
gn clean out/Default         # 清理构建目录

# 查询命令
gn desc out/Default //:target  # 查看目标详情
gn refs out/Default //file.cc  # 查找引用
gn ls out/Default              # 列出所有目标

# 辅助命令
gn format BUILD.gn            # 格式化文件
gn help                       # 帮助信息

团队介绍

智慧家技术平台-应用软件框架开发」主要负责设计工具的研发,包括营销设计工具、家电VR设计和展示、水电暖通前置设计能力,研发并沉淀素材库,构建家居家装素材库,集成户型库、全品类产品库、设计方案库、生产工艺模型,打造基于户型和风格的AI设计能力,快速生成算量和报价;同时研发了门店设计师中心和项目中心,包括设计师管理能力和项目经理管理能力。实现了场景全生命周期管理,同时为水,空气,厨房等产业提供商机管理工具,从而实现了以场景贯穿的B端C端全流程系统。

相关推荐
程序员黑豆2 小时前
鸿蒙应用开发:网络请求三种方式详解(http / rcp / axios)
前端·harmonyos
小雨青年2 小时前
【HarmonyOS 7 沉浸光感深度实战】 02 全局开关、MaterialState 与最小 Demo
华为·harmonyos
lilian2333 小时前
Harmony os 技术实战|拼豆制图06:收藏 ID、生成记录与重启恢复怎么不打架
android·java·数据库·harmonyos
程序员黑豆3 小时前
鸿蒙应用开发之生命周期方法完全指南
前端·harmonyos
HMS Core5 小时前
借助AR Engine人脸识别与跟踪能力,直播不露脸也生动
ar·harmonyos
云端漫步19877 小时前
HarmonyOS NEXT AI 智能生活助手:AI 日程规划
人工智能·华为·生活·harmonyos
达子6669 小时前
第25章_HarmonyOs开发图解之 电话服务
华为·harmonyos
懿路向前10 小时前
【HarmonyOS学习笔记】2026-08-05 | 端插件卡片绑定与跨上下文判断
笔记·学习·ai编程·harmonyos