编写 GN 模板的最佳实践

概览

在 GN 中,模板提供了一种在 GN 的内置目标类型中添加内容的方式。从根本上说,模板是 GN 构建可重复使用函数的主要方式。模板定义位于可导入到目标 .gn 文件中的 .gni (GN import) 文件中。

本文档详细介绍了创建 GN 模板的最佳实践,并在每项最佳实践中都提供了一个示例。这些最佳实践是对 Fuchsia 构建系统政策中所述的最佳实践的补充。

如需了解详情和更完整的示例,请运行 fx gn help template;如需详细了解 GN 功能,请参阅 GN 语言和操作

模板

.gni 中定义模板,在 BUILD.gn 中定义目标

从技术上讲,可以同时导入 .gniBUILD.gn 文件。但最佳做法是在 .gni 文件中定义模板,在 .gn 文件中定义目标。这样可以让用户清楚地了解什么是模板。用户希望导入模板以便使用,但不想导入目标。

文档模板和参数

记录您的模板和参数,包括:

  • 对模板的用途和引入的概念的一般说明。建议提供一个实际使用示例。
  • 所有参数都应进行记录。可以列出常见且只是转发的参数(例如 depsvisibility),参数的含义与其在内置 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/ 等路径下的文件中,从而将强制执行切换为信号传输。

测试模板

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

您不应依赖他人的构建和测试来测试您的模板。拥有自己的测试可使模板更易于维护,因为这样可以更快地验证将来对模板的更改,并且更容易隔离故障。

# //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 转发到内部目标,请执行以下操作:

  1. 内部目标可能无法构建,因为您的用户可能会向您传递 testonly 依赖项。
  2. 用户发现其 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_depsdata_deps

所有接受 deps 的内置规则都接受 public_depsdata_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_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() 一起使用。

相较于 write_file(),首选 generated_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 是常见选择,因为它是执行构建脚本的位置。如需详细了解 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 变体列表,可检查其中一个 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