1. 主页
  2. 文档
  3. Fuchsia OS 简体中文文档 2021
  4. 概念
  5. 构建系统
  6. 编写 GN 模板的最佳实践

编写 GN 模板的最佳实践


概述 {#overview}

在 GN 中,模板(template)提供了一种添加到 GN 内置目标类型的方法。根本上讲,模板是 GN 构建可重用功能的主要方式。模板定义放在 .gni(GN import)文件内,这种文件可以导入 .gn 目标文件。

本文档详细介绍了创建 GN 模板的最佳做法,它们每一条都包含了一个示例。这些最佳实践是对 Fuchsia 构建系统策略中概述的最佳实践的补充。

运行 gn help template 以获取更多信息和更完整的示例,查看 GN 语言与操作以获取有关 GN 特性的更多信息。

模板 {#templates}

.gni 中定义模板,在 BUILD.gn 中定义目标 {#define-templates-in-gni-targets-in-build-gn}

技术上是可以做到同时导入 .gniBUILD.gn 文件的。不过,最佳实践是在 .gni 文件内定义模板,并在 .gn 文件内定义目标。这样一来,用户就可以清楚地知道模板是什么。用户希望导入模板以便使用,而从不想要导入目标。

为模板和参数编写说明文档 {#document-templates-and-args}

请为您的模板和参数编写说明文档,其中包括:

  • 一段模板用途和概念的简单介绍。建议使用一个实际运用的示例。

  • 所有参数都应介绍说明。有些参数比较简单常见(如 depsvisibility),并且它们的含义与内置 GN 规则中的含义一致,这样的参数可以直接列出而无需附加信息。

  • 如果模板生成 metadata,那么应当列出 data_keys

要为您的模板编写说明文档,请在模板定义之前插入注释块以指定您的公共契约(public contract)。

declare_args() {
  # 创建磁盘镜像时分配的字节数。
  disk_image_size_bytes = 1024
}

# 定义一个磁盘镜像文件。
#
# 镜像磁盘文件用于启动虚拟机 bar。
#
# 示例:
# ```
# disk_image("my_image") {
#   sources = [ "boot.img", "kernel.img" ]
#   sdk = false
# }
# ```
#
# 参数
#
#  sources(必需)
#    包含在该镜像中源文件的列表。
#    类型:list(path)
#
#  sdk(可选)
#    该镜像导出至SDK。
#    类型:bool
#    默认:false
#
#  data_deps
#  deps
#  public_deps
#  testonly
#  visibility
#
# 元数据
#
#  files
#    该镜像中显示的文件名。
template("disk_image") {
  ...
}

使用单一 action 模板包装工具 {#wrap-tools-with-a-single-action-template}

每个工具都有一个规范模板,通过一个 action 就可以将其打包。该模板的工作仅仅是将 GN 参数转化为对应工具的 args。这在工具上对于细节设置了一个封装界限,例如将参数转换为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) ]
    }
  }
}

考虑将模板设为私有 {#consider-making-templates-private}

请考虑将名称以下划线开头的模板和变量(例如:template("_private"))设为私有,他们将对 import()(导入)它们的其他文件不可见,但可在定义它们的那个文件中使用。这对于内部帮助模板(此处的“帮助”并不是对用户有所帮助)或您(比如为了在两个模板间共享逻辑而)定义的“本地全局变量”而言有所帮助。

template("coffee") {
  # 例如 coffee 参数有 roast 和 sugar
  ...
  _beverage(target_name) {
    # 以 beverage 的术语来说,如 ingredients 和 temperature
    ...
  }
}

template("tea") {
  # 例如 tea 参数有 loose leaf 和 cream
  ...
  _beverage(target_name) {
    # 以 beverage 的术语来说,如 ingredients 和 temperature
    ...
  }
}

# 我们不想让人直接定义新的 beverage。
# 比如他们可能向 ingredient 列表中既加入 sugar 又加入 salt。
template("_beverage") {
  ...
}

有时您可能无法将模板设为私有,因为它确实需要在其他文件中用到,但您仍希望将其隐藏,因为使用并不意味着是直接的。在这样的情况下,您可以更换方案,通过将您的模板放入形如路径 //build/internal/ 下的文件内的方式达意。

测试您的模板 {#test-your-templates}

请编写需要使用您模板进行构建的测试,或者使用在测试过程中由您模板生成的文件。

您不应当依赖他人的构建和测试来测试您的模板。拥有您自己的测试能使您的模板更容易维护,因为更新对您自己的模板进行的后续改动会变得更快,并且查错会变得更加容易。

# //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" ]
  ...
}

参数 {#parameters}

对必需的参数 assert {#assert-on-required-parameters}

如果您的模板中有必需参数,对他们被定义一事进行 assert(强调说明)。

如果用户忘记指定一个必需的参数,而又没有已定义的“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 {#always-forward-testonly}

在目标上设置 testonly 以防它被非测试目标使用。

如果您的模板没有向内部目标传递 testonly,那么:

  1. 您的内部目标有可能构建失败,因为您的用户可能向您传送 testonly 依赖。
  2. 您将使您的用户发现他们的测试级产品最终变成了生产级产品。

下例示范了如何传递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, "*") 为避免破坏变量将不会传递它。以下是一些解决方式:

# 损坏,不传递 `testonly`
template("my_template") {
  testonly = ...
  action(target_name) {
    forward_variables_from(invoker, "*")
    ...
  }
}

# 有效
template("my_template") {
  testonly = ...
  action(target_name) {
    forward_variables_from(invoker, "*")
    testonly = testonly
    ...
  }
}

# 有效
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 并隐藏内部目标 {#forward-visibility-to-the-main-target-hide-inner-targets}

GN 用户希望能够对任何目标设置 visibility

这一建议与总是传递“testonly”类似,除了前者只应用于主要目标(命名为 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_depsdata_deps {#if-forwarding-deps-also-forward-public_deps-and-data_deps}

所有带有 deps 的内置规则也有 public_depsdata_deps。一些内置规则并不区分 deps 的类型(例如:action()
depspublic_deps 同等对待),而依赖于您生成的目标的则会进行区分(例如:一个依赖于您生成的 action()executable() 会区别对待传递性的 depspublic_deps)。

template("my_template") {
  action(target_name) {
    forward_variables_from(invoker, [
                                       "data_deps",
                                       "deps",
                                       "public_deps",
                                       "testonly",
                                       "Visibility"
                                    ])
    ...
  }
}

目标名称 {#target-names}

定义一个名为 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 是良好的输出名称默认值,但也应提供覆盖功能 {#target_name-is-a-good-default-for-an-output-name-but-offer-an-override}

如果您的模板生成了单一输出,那么选用目标名称作为输出名称是良好的默认行为。但是,由于同一目录下目标名称必须唯一,因此您的用户并不总是能够将他们想用的名字用在目标和输出两者上。

为用户提供覆盖功能是一个很好的最佳实践:

template("image") {
  forward_variables_from(invoker, [ "output_name", ... ])
  if (!defined(output_name)) {
    output_name = target_name
  }
  ...
}

为内部目标名称加上 $target_name 前缀 {#prefix-internal-target-names-with-$target_name}

GN 标签必须唯一,否则您将会收到生成时错误(gen-time error)。如果同一项目中的所有人都遵循了相同的命名约定,那么将减少发生冲突的可能,并且关联目标和其创建的标签这一操作将变得更加容易。

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" ]
  }
}

请勿从目标标签推断输出名称 {#do-not-infer-output-names-from-target-labels}

猜测目标名称和输出名称间的关系这一做法非常具有诱惑性。比如,下面的示例将正常运作:

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 功能和生成 {#gn-functions-and-generation}

只能在对源文件使用 read_file()

read_file() 出现在生成过程中,它不能安全地用于读取生成的文件和构建的输出。它可以用于读取源文件,例如导入构建依赖时读取 manifest 文件或 json 文件。注意 read_file() 不能与 generated_file()write_file() 一同使用。

多用 generated_file(),少用 write_file() {#prefer-generated_file-over-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() 的相对路径 {#prefer-relative-paths-from-rebase-path}

总是在 rebase_path() 中指定一个 new_base(新的基准位置),例如 rebase_path("foo/bar.txt", root_build_dir)。避免其单参数形式,即 rebase_path("foo/bar.txt")

GN 的 rebase_path() 拥有三个参数,其中后两个可选。它的单参数形式返回一个绝对路径,这种做法不推荐。在构建模板和目标中应当避免。new_base 的值会根据实际情况发生变化,而 root_build_dir 则是其常用选项,因为它是构建脚本执行的地方。请在 rebase_path()GN 参考手册中参阅更多信息。

相对路径可以在项目路径或构建输出目录发生改变时保持不变。相较于绝对路径,相对路径有几点优势:

  • 不通过构建输出路径泄露潜在敏感信息,保护用户隐私。
  • 提升内容定址缓存(content-addressed caches)的效率。
  • 使得 bot 间的交互成为可能,例如,一个 bot 跟随另一 bot 的操作运行。

参阅:
rebase_path(x) 返回绝对路径,被认定是有害的?

模式与反面模式 {#patterns-and-anti-patterns}

标签输出 {#target-outputs}

在使用 get_target_outputs() 提取单一元素时,GN 不会允许您对未分配的列表进行下标操作。要解决此问题,您可以使用下面这种不怎么优雅的方法:

# 向列表尾插元素很优雅
deps += get_target_outputs(":some_target")

# 提取单一元素以在变量代换中使用——丑陋但是可靠
_outputs = get_target_outputs(":other_target")
output = _outputs[0]
message = "My favorite output is $output"

# 该表达式是无效的:`output = get_target_outputs(":other_target")[0]`
# GN 不会允许您对右值进行下标操作。

设置操作 {#set-operations}

GN 提供的聚合数据类型为列表(list)和 域(scope),但不提供诸如地图(map)和集合(set)这样的关联类型。有时列表被用来代替集合。下面的示例含有一个构建变量的列表,并检查其中之一是否是“profile”变量:

if (variants + [ "profile" ] - [ "profile" ] != variants) {
  # Do something special for profile builds
  ...
}

这是一种反面模式(anti-pattern,意近“反面教材”)。相反地,变量可以按照如下方式定义:

variants = {
  profile = true
  asan = false
  ...
}

if (variants.profile) {
  # Do something special for profile builds
  ...
}

传递 "*" {#forwarding-*}

forward_variables_from() 将从给定域_或任何外封闭域_中将指定的变量复制到当前域下。除非指定 "*"——这种情况下它将仅从给定域下复制变量。并且它绝不会替换您域中已经存在的变量——那是一个生成时错误。

有时您希望从主调函数复制一切,除了某个你想从任何外封闭域中复制的特定变量。您将会用到这样的模式:

forward_variables_from(invoker, "*", [ "visibility" ])
forward_variables_from(invoker, [ "visibility" ])

exec_script() {#exec-script}

GN 的内置函数 exec_script 是增强 GN 能力的有力工具。与 action() 相同的是,exec_script() 可以调用外部工具。与 action() 不同的是,exec_script() 与构建生成同步地调用外部工具,这意味着您能够在您的 BUILD.gn 逻辑中使用该工具的输出。

由于这造成了生成时期的性能瓶颈(即:fx set 耗时更长),因此该特性必须小心使用。要获取更多信息,请参阅由 Chromium 团队撰写的这篇评论

一份允许列表已被建立在 //.gn。请向 OWNERS 咨询针对该允许列表所做的改动。

标签 ,
这篇文章对您有用吗?

我们要如何帮助您?

Comments

Leave a reply

您的电子邮箱地址不会被公开。 必填项已用*标注