> 文档中心 > OpenHarmony编译链之GN

OpenHarmony编译链之GN

在学习OpenHarmony时,看他她使用GN将整个项目组织起来,于是学习了一下GN,这里将学习的东西记录下

1. GN有什么作用
在学习GN之前我们先来了解下什么是GN,对于一套编译工具,要将源代码编译成最终可以执行文件,需要三个部分构件系统:GN、Ninja、GCC(这里也可以是其他的编译器)

GN作为编译逻辑部分,我理解他就像一个穿糖葫芦的竹签一样,将整个项目穿起来。通过gn gen这个命令,会将这些线索连在一起,生成build.ninja文件。

Ninja作为构建中间部分,他用最简单最快速的方式将编译文件编译参数传递给编译系统,其作用有点像Makefile,编译时,不同的是由Ninja负责执行编译过程。

GCC作为编译部分,通常随编译系统而变化。

好了,有了整体的认识,也知道为啥要学,下面我们就找些资料学习下吧。这里主要参考 Chromium-based projects的文档,相关文档我会放在我的gitee上,方便后续的同学学习。学习资料链接

2. GN学习—组成元素

gn的内部不是很多,即使不学习语法,可以仿写将自己的源文件加入编译。所以这里我们先介绍GN的组成元素,在介绍GN的基本使用,在最后介绍GN的简单语法。在完成这些介绍后,给大家一个使用的案例,这样子比较容易掌握。

  • Targets
    在大多数GN文件中都需要有一编译的目标生成文件,他可以是executable(可执行文件),static_library(静态库),shared_library(动态库)等,除了这些Targets之外还有:

●executable, shared_library, static_library

●loadable_module: 运行时加载模块

●source_set: 被编译的源文件集合,这种编译不会生成任何库文件

●group: 声明一组target,你可以写一个group,里面放不同的库target,使用的时候就用group的名字代表这些targets

●copy: 拷贝文件

●action, action _foreach: 运行一个脚本时所用

●bundle_data, create_bundle: Mac & iOS

●component: shared library or source set depending on mode

●test

●app: executable or iOS application + bundle

●android_apk, generate_jni, etc.: Lots of Android ones!

  • config
    这个关键字的目的是声明一个配置,配置信息可以包括flags,defines,include_dirs等,但是不包括sources和deps/public_deps等依赖性文件。
    config配置应紧挨着使用它的相应target目标之前出现。
    与单个target目标关联的config配置应与target目标同名,并用​​_config​​跟在它后面。如 target名称为foo,则对应的config名称为foo_config。

例子Example for the src/foo/BUILD.gn file:

# Copyright 2016 The Chromium Authors. All rights reserved.# Use of this source code is governed by a BSD-style license that can be# found in the LICENSE file.# Config for foo is named foo_config and immediately precedes it in the file.config("foo_config") {}# Target matching path name is the first target.executable("foo") {      #这里是Targets}# Test for foo follows it.test("foo_unittests") {}config("bar_config") {}source_set("bar") {}
  • Sources
    这个标签的意思是列出来需要编译的源文件,当然,可以在其中使用条件语句进行条件编译
sources = [    "main.cc",  ]  if (use_aura) {    sources += [ "thing_aura.cc" ]  }  if (use_gtk) {    sources += [ "thing_gtk.cc" ]  }
  • Deps
    即编译Targets所用到的依赖,依赖应按字母顺序排列。
    当前文件中的 Deps 应首先写入,并且不能使用文件名限定(仅需要:foo )。
    其他 deps 应始终使用完全限定的路径名,除非出于某种原因需要相对路径名。
 deps = [    ":a_thing",    ":mystatic",    "//foo/bar:other_thing",    "//foo/baz:that_thing",  ]
  • Import
    导入gni文件,这和include类似
import("//foo/bar/baz.gni")  # Even if this file is in the foo/bar directory
  • Variables
    在.gni文件中的顶级局部变量前面加上下划线前缀。此前缀会导致变量无法被导入到其他构建文件。
_this_var_will_not_be_exported = 1but_this_one_will = 2

3. GN学习—快速使用
运行GN(Generate Ninja),生成了一个构建目录,ninja文件将被自动生成,如果你在该目录下进行构建时,文件已经过期,ninja则会自动重新生成,所以你不必重新运行gn

gn gen out/my_buildgn args out/my_build(如:gn is_debug = false out/my_build)

针对指定操作系统或架构交叉编译
运行 gn args out/Default (替换成你的编译目录), 然后为常用的交叉编译选项提供如下参数

target_os = "chromeos"target_os = "android"target_cpu = "arm"target_cpu = "x86"target_cpu = "x64"更多信息见 GN cross compiles.

简单例子
添加一个构建文件
跳转到 examples/simple_build. 这是一个最小化 GN 仓库的根目录.

(PS: 该目录在 GN 源码仓库下 )

在该目录下有一个 tutorial 目录. 下有还没加入构建的 tutorial.cc 文件. 在该目录下创建一个新的 BUILD.gn 文件, 来为该文件生成新的可执行 target .

executable("tutorial") {  sources = [    "tutorial.cc"  ]}

现在我们需要加入该 target. 打开父目录(simple_build)下的BUILD.gn. GN从这个根文件进行加载, 之后加入该文件的所有依赖, 因此我们需要从该文件依赖新的 target.

你可以将新 target 作为依赖添加到simple_build/BUILD.gn文件里现存的 target 上, 但通常将可执行 target 作为另一个可执行 target 的依赖是不合理的(它们不能被链接). 因此我们新建一个 “tools”组, 一个“组”表示一系列不被编译或链接的依赖.

group("tools") {  deps = [    # 这里也可以写全名 “://tutorial:tutorial”.     # 更多信息可见 “gn help labels”    "//tutorial",  ]}

测试添加后的效果
在 simple_build 目录运行:

gn gen outninja -C out tutorialout/tutorial"Hello, world" 应该正确输出在命令行中

注: GN 支持静态库的 target 名不全局唯一. 可以对 ninja 传递带有路径(不带“//”)的标签来构建:
ninja -C out some/path/to/target:my_target

声明依赖
在examples/simple_build/BUILD.gn定义的 target 中, 有一个被静态库定义的函数GetStaticText().

static_library("hello_static") {  sources = [    "hello_static.cc",    "hello_static.h",  ]}

共享库也定义了一个函数GetSharedText():

shared_library("hello_stared") {  sources = [    "hello_shared.cc",    "hello_shared.h",  ]  defines = [ "HELLO_SHARED_IMPLEMENTATION" ]}

这里展示了怎样在预处理器中为 target 添加 define. 如果需要多于一个的 define 或赋值 define , 参照如下形式:

defines = [  “HELLO_SHARED_IMPLEMENTATION",  "ENABLE_DOOM_MELON=0",]

现在来看依赖这两个库的可执行文件

executable("hello") {  sources = [    "hello.cc",  ]  deps = [    ":hello_shared",    ":hello_static",  ]}

测试二进制
在simple_build目录运行:

ninja -C out helloout/hello

注意你不需要重新运行 GN, 当构建文件变化时, GN 会自动重新构建 ninja 文件. 可以通过 ninja 在执行的开始时,在命令行中的输出 [1/1] Regenerating ninja files 来确认.

将设置放到 config 里
库的用户通常需要 complier flags , defines 和应用它们的 include 目录. “config” 命名的设定集合(没有源文件和依赖), 通过将这些设置放到 “config” 中来达到这个目的.

config("my_lib_config") {  defines = [ "ENABLE_DOOM_MELON" ]  include_dirs = [ "//third_party/something" ]}

要把 config 加到 configs 列表里, 来使 config 中的设定在 target 中生效.

static_library("hello_shared") {  ...  # 注 这里通常需要 "+=", 见下文的 "默认 configs”  configs += [:my_lib_config",  ]}config 放在 public_configs 列表中则可在全部依赖它的 target 中生效.static_library("hello_shared") {  ...  public_configs = [    ":my_lib_config",  ]}

public_configs 同样会在当前 target 生效, 因此不需要在两个列表中同时声明.

默认 configs
构建的配置会有一些默认应用到所有 target 的设置. 它们会被设置成 configs 的默认值. 你可以使用 “print” 命令来打印它们(该命令在 debug 中非常使用).

executable("hello") {  print(configs)}

运行 GN 将打印一些类似的结果

$ gn gen out["//build:compiler_defaults", "//build:executable_ldconfig"]Done. Made 5 targets from 5 files in 9mstarget 可以修改这些默认值. 例如, 构建需要通过no_exceptions config 关闭异常, 但 target 可能需要重新启用它们, 为此需要替换默认的 configs 列表.executable("hello") {  ...  configs -= [ "//build:no_exceptions" ]  # Remove global default.  configs += [ "//build:exceptions" ]  # Replace with a different one.}

print 命令也支持字符串差值, 它通过“$“将变量名替换成字符串

print(“The configs for the target $target_name are $configs”)

添加一个新的构建参数
在 declare_args 函数中可以定义需要的参数和它的默认值.

declare_args() {  enable_teleporter = true  enable_doom_melon = false}

查看gn help buildargs来总览它是如何生效的, gn help declare_args来查看声明它们的细节.

在一个作用域中重复定义参数是不允许的, 因此定义参数前需要考虑它的名字和作用域
2. GN学习—语法
GN使用非常简单的动态类型语言。类型是:

布尔(true,false)。
64位有符号整数。
字符串。
列表(任何其他类型)。
范围(Scopes)(有点像字典,仅是内置的东西(built-in stuff))。
有一些内置变量的值取决于当前的环境。了解gn help更多信息。

语言中故意有许多遗漏。例如没有用户定义的函数调用,(模板是最接近的)。按照上述设计理念,如果你需要这样的东西,你可能做错了。

变量sources有一个特殊的规则:赋值给它时,将应用一个排除模式列表。这被设计成自动过滤掉某些类型的文件。见gn help set_sources_assignment_filter和gn help label_pattern了解更多。

语言书呆子的完整语法可以在gn help grammar获取到。

字符串
字符串用双引号括起来,并使用反斜杠作为转义字符。唯一支持的转义序列是:

" (用于直接引用)
$ (字面上的美元符号)
\ (用于文字反斜杠)
任何其他反斜杠的使用都被视为文字反斜杠。所以,例如,\b在模式中使用不需要转义,大多数Windows路径"C:\foo\bar.h"也不需要。

使用$支持简单的变量替换,其中美元符号后的单词被替换为变量的值。如果没有非变量名字符来终止变量名称,可以选择{}包围名称。更复杂的表达式不被支持,仅支持变量名称替换。

a = "mypath"b = "$a/foo.cc"  # b -> "mypath/foo.cc"c = "foo${a}bar.cc"  # c -> "foomypathbar.cc"

您可以使用 “$0xFF” 语法对8位字符进行编码,因此带有换行符(十六进制0A)的字符串会如下所示,“look$0x0Alike$0x0Athis”。

清单
没有办法得到一个列表的长度。如果你发现自己想要做这种事情,那么你就是想在构建中做太多的工作。

列表支持追加:

a = [ "first" ]a += [ "second" ]  # [ "first", "second" ]a += [ "third", "fourth" ]  # [ "first", "second", "third", "fourth" ]b = a + [ "fifth" ]  # [ "first", "second", "third", "fourth", "fifth" ]

将列表追加到另一个列表,是追加第二个列表中的项目,而不是将列表追加为嵌套成员。

您可以从列表中删除项目:

a = [ "first", "second", "third", "first" ]b = a - [ "first" ]  # [ "second", "third" ]a -= [ "second" ]  # [ "first", "third", "fourth" ]

列表中的 - 运算符搜索匹配项并删除所有匹配的项目。从另一个列表中减去一个列表将删除第二个列表中的每个项目。

如果找不到匹配的项目,将会抛出错误,因此您需要事先知道该项目在移除之前确实已经存在。鉴于没有办法测试包含,主要的用例是建立一个文件或标志的主列表,并基于各种条件删除那些不适用于当前版本的构建。

从风格上来说,最好只添加到列表,并让每个源文件或依赖项只出现一次。这与Chrome团队用于GYP的建议相反(GYP倾向于列出所有文件,然后删除条件中不需要的文件)。

列表支持从零开始的下标以提取值:

a = [ "first", "second", "third" ]b = a[1]  # -> "second"

[]运算符是只读的,不能用来改变列表。这个主要的用例是当一个外部脚本返回几个已知的值,并且你想提取它们。

在某些情况下,如果您要添加到列表中,则很容易覆盖列表。为了帮助理解这种情况,将非空列表分配给包含现有非空列表的变量是错误的。如果您想避开此限制,请首先将目标变量分配给空列表。

a = [“one”]a = [“two”]#错误:用非空列表覆盖非空列表。a = []#OKa = [“two”]#OK

请注意,构建脚本的执行没有内在知识的底层数据的意义。例如,这意味着它不知道sources是一个文件名列表。所以,如果你删除一个项目,它必须匹配文字字符串,而不是指定一个不同的名称,那将解析为相同的文件名称。

条件语句
条件看起来像C:

  if(is_linux ||(is_win && target_cpu ==“x86”)){    sources -= [ "something.cc" ]  } else if...{    ...  } else {    ...  }

如果只能在某些情况下声明目标,则可以在大多数地方使用它们,甚至在整个目标周围使用它们。

循环
你可以使用foreach迭代一个列表。这是不鼓励的。构建应该做的大部分事情通常都可以在不做这件事情的情况下表达出来,如果你觉得有必要的话,这可能表明你在元构建中做了太多工作。

foreach(i,mylist){  print(i)  # Note: i is a copy of each element, not a reference to it.}

函数调用
简单的函数调用看起来像大多数其他语言

print("hello, world")assert(is_win, "This should only be executed on Windows")

这些功能是内置的,用户不能定义新的功能。

一些函数在它们下面接受一个由{ }组成的代码块:

static_library(“mylibrary”){  sources = [“a.cc”]}

其中大多数用来定义目标。用户可以使用下面讨论的模板机制来定义新的函数。

确切地说,这个表达式意味着该块成为函数执行的参数。大多数块式函数都会执行块,并将结果范围视为要读取的变量字典。

作用域和执行(Scoping and execution)
文件和函数调用后面跟着{ }块引入新的作用域。作用域是嵌套的。当您读取一个变量时,将会以相反的顺序搜索包含的作用域,直到找到匹配的名称。变量写入总是进入最内层的作用域。

除了最内层的作用域以外,没有办法修改任何封闭作用域。这意味着当你定义一个目标时,例如,你在块内部做的任何事情都不会泄露到文件的其余部分。

if/ else/ foreach语句,即使他们使用{ },不会引入新的范围,所以更改将持续在语句之外。

命名事物
文件和目录名称
文件和目录名称是字符串,并被解释为相对于当前构建文件的目录。有三种可能的形式:

相对名称:

"foo.cc""src/foo.cc""../src/foo.cc"

源代码树绝对名称:

//net/foo.cc”“//base/test/foo.cc”

系统绝对名称(罕见,通常用于包含目录):

“/usr/local/include/”
“/C:/Program Files/Windows Kits/Include”
构建配置
目标
目标是构建图中的一个节点。它通常代表将要生成的某种类型的可执行文件或库文件。目标取决于其他目标。内置的目标类型(请参阅gn help 以获取更多帮助)是:

action:运行一个脚本来生成一个文件。
action_foreach:为每个源文件运行一次脚本。
bundle_data:声明数据加入到Mac / iOS包。
create_bundle:创建一个Mac / iOS包。
executable:生成一个可执行文件。
group:引用一个或多个其他目标的虚拟依赖关系节点。
shared_library:.dll或.so。
loadable_module:.dll或.so只能在运行时加载。
source_set:一个轻量级的虚拟静态库(通常比真正的静态库更可取,因为它的构建速度会更快)。
static_library:.lib或.a文件(通常你会想要一个source_set)。
您可以使用模板来扩展它制作自定义目标类型(请参见下文)。在Chrome中,一些更常用的模板是:

component:源集或共享库,取决于构建类型。
test:测试可执行文件 在移动设备上,这将为测试创建适当的本机应用程序类型。
app:可执行文件或Mac / iOS应用程序。
android_apk:制作一个APK。有很多其他的Android模版,看//build/config/android/rules.gni。
CONFIGS
配置文件是命名对象,用于指定标志集,包含目录和定义。他们可以被应用到一个目标,并推到相关的目标。

要定义一个配置:

config("myconfig") { includes = [ "src/include" ] defines = [ "ENABLE_DOOM_MELON" ]}

要将配置应用于目标:

executable("doom_melon") {  configs = [ ":myconfig" ]}

构建配置文件通常指定设置默认配置列表的目标默认值。目标可以根据需要添加或删除。所以在实践中你通常会使用configs += ":myconfig"追加到默认列表。

请参阅gn help config有关如何声明和应用配置的更多信息。

公共配置
目标可以将设置应用于依赖它的其他目标。最常见的例子是一个第三方目标,它需要一些定义或包含目录头才能正确编译。您希望这些设置既适用于第三方库本身的编译,也适用于使用该库的所有目标。

要做到这一点,你写一个你想要应用的设置的配置:

config("my_external_library_config") {  includes = "."  defines = [ "DISABLE_JANK" ]}

然后这个配置作为“公共”配置被添加到目标。它既适用于目标,也适用于直接依赖目标的目标。

shared_library("my_external_library") {  ...  # Targets that depend on this get this config applied.  public_configs = [ ":my_external_library_config" ]}

依赖目标又可以通过将目标作为“公共”依赖项添加到另一个级别,从而将依赖关系树转发到另一个级别。

static_library("intermediate_library") {  ...  # Targets that depend on this one also get the configs from "my external library".  public_deps = [ ":my_external_library" ]}

通过把它设置成all_dependent_config一个目标可以转发一个配置给所有的依赖者,直到达到一个链接边界为止。这是强烈不鼓励的,因为它将比必要的构建配置超出更多的标志和定义。使用public_deps来控制哪些标志适用于哪里来代替它。

在Chrome中,更喜欢build/buildflag_header.gni用于定义的构建标题头文件系统,以防止大多数编译器定义的错误。

模板
模板是GN重用代码的主要方式。通常情况下,模板会扩展到一个或多个其他目标类型。

# Declares a script that compiles IDL files to source, and then compiles those#source files.template("idl") {  #Always base helper targets on target_name so they're unique。Target name  #will be the string passed as the name when the template is invoked.  idl_target_name =“$ {target_name} _generate”  action_foreach(idl_target_name){    ...  }  #Your template should always define a target with the name target_name.  #When other targets depend on your template invocation, this will be the  #destination of that dependency.  source_set(target_name){    ...    deps = [ ":$idl_target_name" ]  # Require the sources to be compiled.  }}

通常,您的模板定义将放入.gni文件中,用户将导入该文件以查看模板定义:

import("//tools/idl_compiler.gni")idl("my_interfaces") {  sources = [ "a.idl", "b.idl" ]}

当时声明一个模板会在范围内的变量周围创建一个闭包。当模板被调用时,魔术变量invoker被用来从调用范围中读取变量。模板通常会将感兴趣的值复制到自己的范围中:

template("idl") {  source_set(target_name){    sources = invoker.sources  }}

模板执行时的当前目录将是调用的构建文件的目录,而不是模板源文件。这是因为从模板调用者传入的文件是正确的(这通常是模板中大多数文件处理的原因)。但是,如果模板本身有文件(可能会生成一个运行脚本的动作),则需要使用绝对路径(“//foo/…”)来引用这些文件,以说明当前目录在调用时将不可预知。查看gn help template更多信息和更完整的例子。

其他特性
Imports
您可以使用import函数将.gni文件导入到当前作用域。这不是 C++意义上的包含。导入的文件是独立执行的,生成的作用域被复制到当前文件中(C ++在include指令出现的当前上下文中执行包含的文件)。这样可以缓存导入的结果,还可以防止包含多个包含文件在内的一些更“创造性”的用途。

通常情况下,一个.gni会定义构建参数和模板。了解gn help import更多信息。

您的.gni文件可以定义不导出到文件临时变量,通过使用名称中的前面的下划线来包含它,就像_this。

路径处理
通常情况下,您需要创建一个文件名或相对于不同目录的文件名列表。运行脚本时,这种情况尤为常见,这些脚本是以构建输出目录作为当前目录执行的,而构建文件通常是指与其包含的目录相关的文件。

您可以使用rebase_path转换目录。查看gn help rebase_path更多的帮助和例子。将相对于当前目录的文件名转换为相对于根目录的典型用法是:new_paths = rebase_path(“myfile.c”, root_build_dir)

模式
模式用于为自定义目标类型的给定输入集生成输出文件名,并自动从sources变量中移除文件(请参阅参考资料gn help set_sources_assignment_filter)。

他们就像简单的正则表达式。了解gn help label_pattern更多信息。

执行脚本
有两种方法来执行脚本。GN中的所有外部脚本都是Python。第一种方法是作为构建步骤。这样的脚本将需要一些输入,并生成一些输出作为构建的一部分。调用脚本的目标是使用“action”目标类型声明的(请参阅参考资料gn help action)。

执行脚本的第二种方法是在构建文件执行期间同步。这在某些情况下是必要的,以确定要编译的文件集合,或获取构建文件可能依赖的某些系统配置。构建文件可以读取脚本的标准输出(stdout)并以不同的方式对其执行操作。

同步脚本的执行由exec_script函数完成(详见gn help exec_script参考资料)。因为同步执行一个脚本需要暂停当前的构建文件执行,直到Python进程完成执行,依靠外部脚本是慢的,应该尽量减少。

为了防止滥用,允许调用的文件exec_script可以在顶层.gn文件中列入白名单。Chrome做到这一点需要额外的代码审查这样的补充。看gn help dotfile。

您可以同步读取和写入在同步运行脚本时不鼓励但偶尔需要的文件。典型的用例是传递一个比当前平台的命令行限制长的文件名列表。请参阅gn help read_file以及gn help write_file如何读取和写入文件。如果可能,应该避免这些功能。

超过命令行长度限制的操作可以使用响应文件绕过此限制,而不同步写入文件。看gn help response_file_contents。

与Blaze的区别和相似之处
Blaze是Google的内部构建系统,现在已经作为Bazel公开发布。它启发了一些其他系统,如Pants和Buck。

在Google的同类环境中,对条件的需求非常低,并且可以通过少量的手段(abi_deps)来获得。Chrome使用各地的条件,需要添加这些是文件看起来不同的主要原因。

GN还增加了“配置”的概念来管理一些棘手的依赖和配置问题,同样不会出现在服务器上。Blaze有一个“配置”的概念,就像一个GN工具链,但内置在工具本身。GN工具链的工作方式是试图以一种简洁的方式将这个概念分离到构建文件中的结果。

GN保留了一些GYP概念,比如“全部依赖”设置,这些设置在Blaze中有些不同。这部分是为了使现有的GYP代码更容易转换,GYP结构通常会提供更细粒度的控制(根据具体情况而定,好或坏)。

GN也使用GYP名称,比如“sources”而不是“srcs”,因为缩写似乎是不必要的,尽管它使用了Blaze的“deps”,因为“dependencies”很难打字。Chromium还在一个目标中编译多种语言,因此指定目标名称前缀的语言类型被删除(例如,从cc_library)。