概览
在 GN 中,模板提供了一种扩展 GN 内置目标类型的方法。从根本上讲,模板是 GN 构建可重用函数的主要方式。模板定义位于可导入到目标 .gn 文件中的 .gni(GN 导入)文件中。
本文档详细介绍了创建 GN 模板的最佳实践,每项最佳实践都包含一个示例。除了 Fuchsia 构建系统政策中概述的最佳实践之外,还应遵循以下最佳实践。
运行 fx gn help template 可获取更多信息和更完整的示例,如需详细了解 GN 功能,请参阅 GN 语言和操作。
模板
在 .gni 中定义模板,在 BUILD.gn 中定义目标
从技术上讲,可以同时导入 .gni 和 BUILD.gn 文件。不过,最佳实践是在 .gni 文件中定义模板,在 .gn 文件中定义目标。这样,用户可以清楚地知道哪些是模板。用户希望导入模板以便使用,但绝不会希望导入目标。
文档模板和实参
记录模板和实参,包括:
- 对模板的用途和所引入的概念进行一般性说明。建议提供实际使用示例。
- 所有形参都应记录在案。对于常见且只是转发的形参(例如
deps或visibility),如果其含义与内置 GN 规则中的含义一致,则可以列出,无需提供其他信息。 - 如果模板生成
metadata,,则应列出data_keys。
如需记录模板,请在模板定义前面插入注释块以指定公共合约。
declare_args() {
# The amount of bytes to allocate when creating a disk image.
disk_image_size_bytes = 1024
}
# Defines a disk image file.
#
# Disk image files are used to boot the bar virtual machine.
#
# Example:
# ```
# disk_image("my_image") {
# sources = [ "boot.img", "kernel.img" ]
# sdk = false
# }
# ```
#
# Parameters
#
# sources (required)
# List of source files to include in the image.
# Type: list(path)
#
# sdk (optional)
# This image is exported to the SDK.
# Type: bool
# Default: false
#
# data_deps
# deps
# public_deps
# testonly
# visibility
#
# Metadata
#
# files
# Filenames present in this image.
template("disk_image") {
...
}
使用单个操作模板封装工具
对于每个工具,都有一个使用 action 将其封装起来的规范模板。此模板的任务是将 GN 参数转换为工具的 args,仅此而已。这会围绕工具设置一个封装边界,以用于将参数转换为实参等详细信息。
请注意,在此示例中,我们在一个文件中定义了 executable(),在另一个文件中定义了 template(),因为模板和目标应分开。
# //src/developer_tools/BUILD.gn
executable("copy_to_target_bin") {
...
}
# //src/developer_tools/cli.gni
template("copy_to_target") {
compiled_action(target_name) {
forward_variables_from(invoker, [
"data_deps",
"deps",
"public_deps",
"testonly",
"visibility"
])
assert(defined(invoker.sources), "Must specify sources")
assert(defined(invoker.destinations), "Must specify destinations")
tool = "//src/developer_tools:copy_to_target_bin"
args = [ "--sources" ]
foreach(source, sources) {
args += [ rebase_path(source, root_build_dir) ]
}
args += [ "--destinations" ]
foreach(destination, destinations) {
args += [ rebase_path(destination, root_build_dir) ]
}
}
}
考虑将模板设为私密
名称以英文下划线开头的模板和变量(例如 template("_private"))被视为私有,不会对 import() 它们的其他文件显示,但可以在定义它们的同一文件中使用。这对于内部辅助模板或“局部全局变量”非常有用,例如,您可能会定义这些变量来在两个模板之间共享逻辑,但辅助变量对用户来说没有用。
template("coffee") {
# Take coffee parameters like roast and sugar
...
_beverage(target_name) {
# Express in beverage terms like ingredients and temperature
...
}
}
template("tea") {
# Take tea parameters like loose leaf and cream
...
_beverage(target_name) {
# Express in beverage terms like ingredients and temperature
...
}
}
# We don't want people directly defining new beverages.
# For instance they might add both sugar and salt to the ingredients list.
template("_beverage") {
...
}
有时,您无法将模板设为私有,因为实际上需要从不同的文件中使用该模板,但您仍然希望隐藏该模板,因为它不应直接使用。在这种情况下,您可以将强制执行模式替换为信号模式,方法是将模板放在路径(例如 //build/internal/)下的文件中。
测试模板
编写使用模板进行构建的测试,或在测试过程中使用模板生成的文件。
您不应依赖其他人的 build 和测试来测试您的模板。 拥有自己的测试可让您的模板更易于维护,因为这样可以更快地验证模板的未来更改,并且更容易隔离故障。
# //src/drinks/coffee.gni
template("coffee") {
...
}
# //src/drinks/tests/BUILD.gni
import("//src/drinks/coffee.gni")
coffee("coffee_for_test") {
...
}
test("coffee_test") {
sources = [ "taste_coffee.cc" ]
data_deps = [ ":coffee_for_test" ]
...
}
参数
对必需参数进行断言
如果您的模板中有必需参数,请确保已定义这些参数。assert
如果用户忘记指定必需的参数,并且没有定义断言,他们将无法获得清晰的错误说明。使用断言可让您提供有用的错误消息。
template("my_template") {
forward_variables_from(invoker, [ "sources", "testonly", "visibility" ])
assert(defined(sources),
"A `sources` argument was missing when calling my_template($target_name)")
}
template("my_other_template") {
forward_variables_from(invoker, [ "inputs", "testonly", "visibility" ])
assert(defined(inputs) && inputs != [],
"An `input` argument must be present and non-empty " +
"when calling my_template($target_name)")
}
始终转接 testonly
在目标上设置 testonly 可防止非测试目标使用该目标。如果您的模板未将 testonly 转发到内部目标,则:
- 您的内部目标可能会构建失败,因为您的用户可能会传递
testonly依赖项。 - 当用户发现其
testonly制品最终出现在生产制品中时,他们会感到惊讶。
以下示例展示了如何转发 testonly:
template("my_template") {
action(target_name) {
forward_variables_from(invoker, [ "testonly", "deps" ])
...
}
}
my_template("my_target") {
visibility = [ ... ]
testonly = true
...
}
请注意,如果内部操作的父级范围定义了 testonly,则 forward_variables_from(invoker, "*") 不会转发它,因为这样可以避免覆盖变量。以下是一些解决此问题的模式:
# Broken, doesn't forward `testonly`
template("my_template") {
testonly = ...
action(target_name) {
forward_variables_from(invoker, "*")
...
}
}
# Works
template("my_template") {
testonly = ...
action(target_name) {
forward_variables_from(invoker, "*")
testonly = testonly
...
}
}
# Works
template("my_template") {
testonly = ...
action(target_name) {
forward_variables_from(invoker, "*", [ "testonly" ])
forward_variables_from(invoker, [ "testonly" ])
...
}
}
硬编码 testonly = true 的模板是唯一的例外,因为它们绝不应在生产目标中使用。例如:
template("a_test_template") {
testonly = true
...
}
将点击事件转发 visibility 到主要目标,并隐藏内部目标
GN 用户希望能够针对任何目标设置 visibility。
此建议与始终向前测试类似,但仅适用于主要目标(名为 target_name 的目标)。其他目标的 visibility 应受到限制,这样您的用户就无法依赖不属于合同范围的内部目标。
template("my_template") {
action("${target_name}_helper") {
forward_variables_from(invoker, [ "testonly", "deps" ])
visibility = [ ":*" ]
...
}
action(target_name) {
forward_variables_from(invoker, [ "testonly", "visibility" ])
deps = [ ":${target_name}_helper" ]
...
}
}
如果转发 deps,则同时转发 public_deps 和 data_deps
所有采用 deps 的内置规则都采用 public_deps 和 data_deps。有些内置规则不会区分依赖项的类型(例如,action() 会同等对待 deps 和 public_deps)。但对生成的目标的被依赖项可能会区分(例如,依赖于生成的 action() 的 executable() 会区别对待传递性 deps 和 public_deps)。
template("my_template") {
action(target_name) {
forward_variables_from(invoker, [
"data_deps",
"deps",
"public_deps",
"testonly",
"Visibility"
])
...
}
}
目标名称
定义名为 target_name 的内部目标
您的模板应至少定义一个名为 target_name 的目标。这样一来,用户就可以通过名称调用模板,然后在依赖项中使用该名称。
# //build/image.gni
template("image") {
action(target_name) {
...
}
}
# //src/some/project/BUILD.gn
import("//build/image.gni")
image("my_image") {
...
}
group("images") {
deps = [ ":my_image", ... ]
}
target_name 是一个不错的输出名称默认值,但应提供覆盖选项
如果模板生成单个输出,那么使用目标名称来选择输出名称是一种不错的默认行为。不过,目标名称在目录中必须是唯一的,因此用户无法始终将所需的名称同时用于目标和输出。
提供覆盖机制是一种很好的最佳实践:
template("image") {
forward_variables_from(invoker, [ "output_name", ... ])
if (!defined(output_name)) {
output_name = target_name
}
...
}
为内部目标名称添加 $target_name 前缀
GN 标签必须是唯一的,否则您会收到生成时错误。如果同一项目中的每个人都遵循相同的命名惯例,则发生冲突的可能性会降低,并且更容易将内部目标名称与创建它们的目标相关联。
template("boot_image") {
generate_boot_manifest_action = "${target_name}_generate_boot_manifest"
action(generate_boot_manifest_action) {
...
}
image(target_name) {
...
deps += [ ":$generate_boot_manifest_action" ]
}
}
不从目标标签推断输出名称
我们很容易会假设目标名称和输出名称之间存在某种关系。例如,以下示例可以正常运行:
executable("bin") {
...
}
template("bin_runner") {
compiled_action(target_name) {
forward_variables_from(invoker, [ "testonly", "visibility" ])
assert(defined(invoker.bin), "Must specify bin")
deps = [ invoker.bin ]
tool = root_out_dir + "/" + get_label_info(invoker.foo, "name")
...
}
}
bin_runner("this_will_work") {
bin = ":bin"
}
不过,以下示例会产生生成时错误:
executable("bin") {
output_name = "my_binary"
...
}
template("bin_runner") {
compiled_action(target_name) {
forward_variables_from(invoker, [ "testonly", "visibility" ])
assert(defined(invoker.bin), "Must specify bin")
tool = root_out_dir + "/" + get_label_info(invoker.bin, "name")
...
}
}
# This will produce a gen-time error saying that a file ".../bin" is needed
# by ":this_will_fail" with no rule to generate it.
bin_runner("this_will_fail") {
bin = ":bin"
}
以下是解决此问题的一种方法:
executable("bin") {
output_name = "my_binary"
...
}
template("bin_runner") {
compiled_action(target_name) {
forward_variables_from(invoker, [ "testonly", "visibility" ])
assert(defined(invoker.bin), "Must specify bin")
tool = bin
...
}
}
bin_runner("this_will_work") {
bin = "$root_out_dir/my_binary"
}
GN 函数和生成
仅将 read_file() 与源文件搭配使用
read_file() 在生成期间发生,无法安全地用于从生成的文件或 build 输出中读取数据。它可以用于读取源文件,例如读取清单文件或用于填充 build 依赖项的 JSON 文件。值得注意的是,read_file() 不能与 generated_file() 或 write_file() 结合使用。
首选 generated_file(),而不是 write_file()
一般来说,建议您使用 generated_file() 而不是 write_file()。generated_file() 提供了更多功能,并解决了 write_file() 的一些难题。例如,generated_file() 可以并行执行,而 write_file() 在生成时是串行执行的。
这两个命令的结构非常相似。例如,您可以将此 write_file() 实例:
write_file("my_file", "My file contents")
转换为以下 generated_file() 实例:
generated_file("my_file") {
outputs = [ "my_file" ]
contents = "My file contents"
}
首选 rebase_path() 的相对路径
请始终在 rebase_path() 中指定 new_base,例如 rebase_path("foo/bar.txt", root_build_dir)。避免使用单参数形式,即 rebase_path("foo/bar.txt")。
GN 的 rebase_path() 有三个形参,后两个是可选的。它的单形参形式会返回一个绝对路径,但已被弃用。请避免在 build 模板和目标中使用它。new_base 的值因具体情况而异,root_build_dir 是一个常见的选择,因为 build 脚本是在此处执行的。如需详细了解 rebase_path(),请参阅其 GN 参考。
当项目或 build 输出目录的路径发生变化时,相对路径可以保持不变。与绝对路径相比,相对路径具有以下优势:
- 通过不泄露 build 输出中路径的潜在敏感信息来保护用户隐私。
- 提高了内容寻址缓存的效率。
- 实现聊天机器人之间的互动,例如,一个聊天机器人执行另一聊天机器人执行的操作。
另请参阅:rebase_path(x) 返回绝对路径是否被认为有害?
模式和反模式
目标输出
使用 get_target_outputs() 提取单个元素时,GN 不允许您在赋值之前对列表进行下标运算。如需解决此问题,您可以采用以下不太优雅的解决方法:
# Appending to a list is elegant
deps += get_target_outputs(":some_target")
# Extracting a single element to use in variable substitution - ugly but reliable
_outputs = get_target_outputs(":other_target")
output = _outputs[0]
message = "My favorite output is $output"
# This expression is invalid: `output = get_target_outputs(":other_target")[0]`
# GN won't let you subscript an rvalue.
此外,get_target_outputs() 还存在一些令人头疼的限制:
- 仅支持
copy()、generated_file()和action()目标平台。 - 只能查询在同一
BUILD.gn文件中定义的目标。
因此,您经常会在另一个 BUILD.gn 文件中发现一个目标输出路径被硬编码。这会造成脆弱的合约。当合约中断时,很难对中断进行问题排查。请尽可能避免这样做,如果无法避免,请添加大量内嵌文档。
检查类型是否为字符串
虽然 GN 不允许进行全面的类型检查,但您可以通过编写以下行来检查变量是否为字符串(且仅为字符串):
if (var == "$var") {
# Execute code conditional on `var` type being string
}
检查 var 是否为单例列表
同样,您可以按如下方式检查变量是否为单例列表:
if (var == [var[0]]) {
# Execute code conditional on `var` type being a singleton list
}
不过请注意,如果类型不是列表或为空,则会发生崩溃。
设置操作
GN 提供列表和范围作为聚合数据类型,但不提供关联类型(如映射或集)。有时会使用列表代替集。以下示例包含一个 build 变体列表,并检查其中一个变体是否为“profile”变体:
if (variants + [ "profile" ] - [ "profile" ] != variants) {
# Do something special for profile builds
...
}
这是一种反模式。相反,变体可以定义如下:
variants = {
profile = true
asan = false
...
}
if (variants.profile) {
# Do something special for profile builds
...
}
正在转发 "*"
forward_variables_from() 将指定变量从给定范围 或任何封闭范围复制到当前范围。除非您指定 "*",在这种情况下,它只会直接复制给定范围内的变量。并且它绝不会覆盖已在您的作用域中的变量,否则会发生生成时错误。
有时,您可能希望复制调用者中的所有内容,但要从任何封闭范围内复制某个特定变量。您会遇到以下模式:
forward_variables_from(invoker, "*", [ "visibility" ])
forward_variables_from(invoker, [ "visibility" ])
exec_script()
GN 的内置函数 exec_script 是一种强大的工具,可用于增强 GN 的功能。与 action() 类似,exec_script() 可以调用外部工具。与 action() 不同,exec_script() 可以与 build 生成同步调用该工具,这意味着您可以在 BUILD.gn 逻辑中使用该工具的输出。
由于这会在生成时间(即 fx set 需要更长时间)内造成性能瓶颈,因此必须谨慎使用此功能。如需了解详情,请参阅 Chromium 团队的这篇博文。
//.gn 中已设置许可名单。请参阅 OWNERS,了解对此许可名单所做的更改。