编写 GN 模板的最佳实践

概览

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

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