编写 GN 模板的最佳实践

概览

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

本文档详细介绍了创建 GN 模板的最佳做法,以及每个 包括一个例子。这些最佳做法是 Fuchsia 构建系统政策中列出的做法。

运行 fx gn help template 以获取更多信息和更完整的示例,并查看 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()操作的文件将看不到这些内容,但可以 使用的 ID 相同。这对于内部帮助程序模板或 例如,您可以定义一些“局部全局变量”, 帮助程序对用户无用的情况。

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 转发到内部目标,则:

  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

这条建议与“始终转发 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

所有接受 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 依赖项的清单文件或 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() 有三个参数,后两个是可选参数。 其单参数形式返回一个绝对路径, 即将被弃用。避免在构建模板和目标中使用。 new_base 的值因具体情况而异,root_build_dir 是 因为它是执行构建脚本的位置。查看更多 有关 rebase_path() 的信息,请参阅 GN 参考文档

项目或构建输出的路径时,相对路径可以保持不变 目录更改。与绝对路径相比,它有一些优势:

  • 保护用户隐私,避免将可能敏感的信息从 构建输出中的路径
  • 提高了内容寻址缓存的效率。
  • 使聊天机器人之间的互动成为可能,例如,一个聊天机器人执行 关注另一个聊天机器人。

另请参阅: 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 需要 因此请务必谨慎使用此功能。 如需了解详情,请参阅 这篇书面报道

已在 //.gn 中设置许可名单。如需了解变更内容,请咨询OWNERS 针对该许可名单所做的修改