编写 GN 模板的最佳实践

概览

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

本文档详细介绍了创建 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/)下的文件中。

测试模板

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

您不应依赖其他人的 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

此建议与始终向前测试类似,但仅适用于主要目标(名为 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() 结合使用。

首选 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,了解对此许可名单所做的更改。