本文进行的讨论假设您对于 GN 的语法和概念已经基本熟悉。这篇对于 GN 的介绍可以为您提供上述背景知识。
GN 使用一种模板结构将构建的细节对于终端用户进行抽象。下面是 Zircon GN 定义的模板的一个子集,它侧重于 Zircon 黑客最可能进行交互的那些模板。
//zircon/
前缀
同在介绍中讨论的一样,GN 使用“绝对于源的”路径,它们看起来形如 //a/b/c
。在 Zircon GN 文件中,我们从不使用 //
。作为替代,请使用 //zircon/foo
来指代 //zircon/foo
,例如:"//zircon/system/ulib/zircon"
。
executable()
和 test()
产生二进制文件的主要目标格式是 executable()
(可执行文件)。这会从列出的源生成一个可执行二进制文件。Zircon 构建也提供了一种方法来指示镜像中的位置,在此二进制文件应当通过目标域中的 install_path
变量安装。install_path
可以是:
- 字符串:与 BOOTFS 根目录有关的路径(无开头的
/
) - 忽略项:使用默认路径
bin/<binary_name>
false
:完全不安装该文件
构建也提供了一个 test()
目标,它与 executable()
相同,除了它设置了 testonly = true
,并且其默认 install_path
为 test/<binary_name>
而非 bin/<binary_name>
。
test()
可以用于在 Zircon 上,或者在主机端运行的测试程序。事实上,相同的 test()
目标可以用于在两种情况下构建相同的测试程序而无需额外工作。(到达该目标的依赖会决定其为主机构建、为 Zircon 构建还是为两者同时构建。)
library()
library()
模板是针对 Zircon 传统中任何种类“库(library)”的,无论是内核、Zircon 用户代码还是主机端代码。“库”的基本含义是有一个公共头文件的 include/
子目录。将该 library()
对象列入它们 deps
内的项目将自动为其 include/
目录获得 -I
开关。
语法最为简洁的默认情况是纯静态用户空间库(static-only userland library)。要使库可用作共享库,只需要添加行 shared = true
。同样地,要使库可用于主机端,只需要添加行 host = true
。这些都是对默认选项 static = true
的补充,它可以使库可用于用户空间静态链接。对于从不应当静态链接的库(除了主机端货内核使用),您可以使用 static = false
替换默认值。
对于内核中的库,设置 kernel = true
。不论是内核专用库还是内核与用户(或/与主机)共享的代码,都应如此。设置 kernel = true
会改变默认值为 static = false
,因此如果要使库能够在内核或用户空间使用,那么除了 kernel = true
外,您还必须显式地设置 static = true
(除非您设置 shared = true
并且希望禁止该库在用户空间的静态链接)。
注意:对于不提供 include/
子目录的内核模块,请使用 source_set()
代替 library()
。
下面是一个范例,它展示了所有必要的选项。多数实际目标并不会比 sources
列表和 deps
列表复杂多少。
library("foo") {
# 静态时构建“libfoo.a”,共享时构建“libfoo.so”。
static = true # 默认,除非 kernel = true,否则忽略:构建用户空间 libfoo.a
shared = true # 如果忽略则为 false:构建用户空间 libfoo.so
kernel = true # 如果忽略则为 false:能从内核使用
host = true # 如果忽略则为 false:能在主机工具中使用
sources = [
"foo.c",
"bar.cpp",
]
deps = [
# 可以将此指向本地定义的 `source_set()` 或其他 `library()` 目标。
":foo_minimal", # 在相同的 BUILD.gn 文件中定义。
"foobar_subsystem", # 相对于此处,在 foobar_subsystem/BUILD.gn 中定义。
# 显式地链入 libbar.a,即使 libbar.so 可用。
"//zircon/system/ulib/bar:static",
# 显式地将 libbaz.so 作为共享库。
"//zircon/system/ulib/baz:shared",
# 带 -Isystem/ulib/bozo/include 编译,但不链入任何内容。
# 这不应当常用在 `deps` 中,而是仅用在 `public_deps` 中。
"//zircon/system/ulib/bozo:headers",
# 让 system/ulib/quux/BUILD.gn 决定该库的基准是静态的还是共享的。
# (目前为止,如果共享库启用了,那么定义 `library()` 将总是偏向于共享库;
# 默认为静态,但是如果有用,也可以很容易地添加构建共享选项。)
"//zircon/system/ulib/quux",
# `library("quextras")` 出现在 system/ulib/quux/BUILD.gn,因为 quux 和 quextras
# 希望共享一些私有代码,或者出于任何原因,我们断定将它们放入单独的目录是正确的做法。
# 由于我们不使用具有其目录名称的目标,因此 `:name` 语法在 BUILD.gn 文件中仅选择特定的目标。
# 对于派生目标名称,我们在前缀前使用 `.`。
# 实际上,“quux:headers” 仅是“quux:quux.headers”等的一个别名。
"//zircon/system/ulib/quux:quextras",
"//zircon/system/ulib/quux:quextras_more.static",
"//zircon/system/ulib/quux:quextras_way_more.shared",
# 这是一个进行 `static=false shared=true` 设置的 `library()`,因此这里的 `zircon:static`
# 不会起作用,而 `zircon:shared` 会起作用。
"//zircon/system/ulib/zircon",
]
# 各模块编译标志(flag)总是可选的。
# *注意*:对于标志顺序有关的情况,有必要使用 config() 代替。
cflags = [ "-Wfoo", "-fbar" ]
cflags_cc = [ "-fonly-for-c++" ]
cflags_c = [ "-fonly-for-c" ]
asmflags = [ "-Wa,--some-as-switch" ]
ldflags = [ "-Wl,--only-affects-shlib-link" ]
}
下面是一个高度删节的内核模块实际用例:
# deps = [ "//zircon/kernel/object" ] gets -Ikernel/object/include
library("object") {
kernel = true
sources = [
"buffer_chain.cpp",
"process_dispatcher.cpp",
]
deps = [
"//zircon/kernel/dev/interrupt",
"//zircon/system/ulib/fbl",
]
}
注意 system/ulib/fbl
不是 kernel/lib/fbl
:这一 fbl
适用于全部。下面是一个针对该情况的高度删节的用例:
library("fbl") {
kernel = true
static = true
sources = [
"alloc_checker.cpp",
]
if (is_kernel) {
sources += [
"arena.cpp",
"arena_tests.cpp",
]
} else {
sources += [ "string.cpp" ]
}
}
实际的 fbl
是一个糟糕的示例,因为它还存在其他问题,但是这说明了共享代码的库怎样才能在使用一个库目标来描述内核和用户空间化身(incarnation)的情况下通过一个 BUILD.gn
文件获得维护。它们共享一切,但可以根据 is_kernel
条件按需要产生区别。
库定义目标的一个标准集合(如果相关):
$target_name.headers
总是提供的,仅是为了获取头部而不将其链入$target_name.static
是提供的,如果static = true
(默认值)$target_name.shared
是提供的,如果shared = true
如果库是文件中的主目标(例如://zircon/foo:foo
)(也是通常情况),那么 static
、shared
和 headers
子目标也有别名 //zircon/foo:static
、//zircon/foo:shared
和 //zircon/foo:headers
。
针对头部依赖的 public_deps
作为对 deps
和 data_deps
的补充,GN 也拥有 public_deps
。它用在目标将依赖置于其公共头部文件中,并需要将该依赖的设置传递至依赖链中的时候。public_deps
的每处使用都应当附上注释,解释其必要性:
例如,library("async-loop")
含有下面的内容:
public_deps = [
# <lib/async-loop/loop.h> has #include <lib/async/dispatcher.h>.
"//zircon/system/ulib/async:headers",
]
source_set()
和 static_library()
一些代码没有包含(include)目录,它们可以直接使用原生 GN source_set()
(源集合)或 static_library()
(静态库)目标。
源集合(source set)(参阅 gn help source_set
)是一种创建文件逻辑分组或缩小编译开关范围(to scope compilation switches narrowly)。目标文件将会直接链入最终的二进制文件,而无需通过任何中间库。相比之下,静态库中的文件只是按需取用以解析符号。
-
内核自身中的代码应当总是使用
source_set
。当前,静态库和内联汇编的交互很差。 -
创建测试组时,必须使用
source_set
,因为测试工具依赖于静态初始化器,而静态库链接规则会剥除测试(strip the tests)。所有内核代码。 -
static_library
应当用于形如库或其一部分的高层次事物中。死代码剥除(dead code stripping)更有效率,并且在一些代码不需要之时能够产生更快的链接和更小的二进制程序。
source_set("some_code") {
sources = [
"this.c",
"that.cpp",
]
}
loadable_module()
目前为止,它并没有在 Zircon 构建中用到,但尚有可能。可加载模块(loadable module)是一种共享对象,它不直接链接,而是通过 dlopen()
之类方式动态加载。
可加载模块对待 install_path
参数的方式与 executable()
(可执行文件)相同。但是它没有默认路径,因此除非您显式地提供路径,否则就等同于 install_path = false
。
Zircon 设备驱动是可加载模块,但是它们有自己的特殊模板可供使用,而非 loadable_module()
。
driver()
和 test_driver()
驱动(driver)是具有一些特殊支持和限制的可加载模块。
- 它们为驱动将默认
install_path
(安装路径)处理得当,这样它们就可以被devmgr
发现。 - 它们隐式地依赖
libdriver
,因此不应将其列入deps
。 - 它们隐式地使用静态 C++ 标准库。
test_driver()
(测试驱动)之于 driver()
(驱动)就如同 test()
(测试)之于 executable()
。
driver("fvm") {
sources = [
"fvm.cpp",
]
deps = [
"//src/lib/storage/fs/cpp",
"//zircon/system/ulib/ddktl",
"//zircon/system/ulib/zircon",
]
}
resources()
和 firmware()
resource()
(资源)目标声明了某个可能在 BOOTFS 镜像中需要但不会在构建中直接造成后果的文件。规则的样式仿佛它是从源文件到构建输出文件的一份拷贝一样;它以 GN 原生的 copy()
规则为模型,而 gn help copy
解释了其语法的成因。outputs
是单元素列表,其中包含了 BOOTFS 中的路径。
import("//zircon/public/gn/resource.gni")
resource("tables") {
sources = [
"data.tbl",
]
outputs = [
"data/some_lib/data_v1.tbl",
]
}
resource()
得目的是用来列入是用来该数据的目标的 data_deps
中:
library("uses_tables") {
sources = [
"read_table.cc",
]
data_deps = [
":tables",
]
}
这可以是 library()
、executable()
、source_set
等等。好的做法是将 data_deps
放入最细粒度的包含在运行时使用该文件代码的目标中。这样做能够确保相关资源在运行时可用。
如果资源由构建生成,那么 sources
列表中的路径将标明其于构建目录下的位置,通常会使用 $target_out_dir
或 $target_gen_dir
。这种情况下,resource()
也必须拥有包含生成该文件的目标的 deps
列表。
构建也允许一种特殊类型的资源,它从依赖图中生成。使用 generated_resource()
会创建一个资源文件,它用于 data_deps
中,和通常的 resource()
一样,但是它不会使用现存源文件,而是在 gn gen
时期使用固定内容或基于元数据集(metadata collection)生成一个文件(参阅 gn help generated_file
以获取细节)。
firmware()
是 resource()
的一种特殊情况变体,它面向驱动。它将资源文件放在 /lib/firmware/$path
,其中 $path
是相对于 /lib/firmware
根目录中资源的相对路径。这模仿了 devhost
中的调用传统,devhost
中的驱动是用相对路径调用 load_firmware(...)
。
fidl_library()
该模板允许 FIDL 库的定义和与其相关的绑定。声明 fidl_library()
目标将导致构建为所有支持的语言生成绑定。
注意:要是用该模板,您必须导入 fidl.gni
文件域。
import("//zircon/public/gn/fidl.gni")
# 定义在 //zircon/system/fidl/fuchsia-io/BUILD.gn 中
fidl_library("fuchsia-io") {
sources = [
"io.fidl",
]
public_deps = [
"//zircon/system/fidl/fuchsia-mem",
]
}
注意 public_deps
的使用。当 FIDL 库的源文件具有 using other_library;
时,就等同于在公共头部使用了 #include <other_library/header>
的 C/C++ 库。因为这对于 FIDL(和 Banjo)库非常常见,所以我们不需要在每种类似的简单模式情况下都进行注释。
根据定义的绑定,上述示例将产生形如 //zircon/system/fidl/fuchsia-io:fuchsia-io.<language>
的目标集合,或者在目标名称与上述目录名称相同的情况下,则为 //zircon/system/fidl/fuchsia-io:<language>
。
如今常见的情况是 "//zircon/system/fidl/fuchsia-io:c"
。
banjo_library()
Banjo 库定义与 FIDL 库相似。banjo_library()
目标将为所有支持的语言产生绑定,尽管其支持语言集与 FIDL 的会有所不同。
import("//zircon/public/gn/banjo.gni")
banjo_library("ddk-driver") {
sources = [
"driver.banjo",
]
}
目前,在 deps
中不使用 :<language>
前缀直接列出目标会同时得到 C 和 C++ 绑定。这一情况在近期可能会发生改变,以跟进 FIDL 模型:确切指定您所依赖的绑定。
参阅前文关于 public_deps
的内容。其在 banjo_library()
中的用法与其在 fidl_library()
中的用法完全相同。
- 中文文档地址:https://fuchsia-china.com/docs/zh-hans/concepts/build_system/zircon_gn/
- GitHub协作地址: https://github.com/FuchsiaOS/FuchsiaOS-docs-zh_CN/blob/2021/concepts/build_system/zircon_gn.md
- 原始英文地址:https://fuchsia.googlesource.com/fuchsia/+/refs/heads/main/docs/concepts/build_system/zircon_gn.md
- 翻译者:Whyto 校稿者:N0B8D1
Comments