封闭的构建操作

Fuchsia 的构建系统使用一种工具跟踪构建操作执行的文件系统操作,以便正确检测这些构建操作并完全说明其输入和输出。

如果您遇到如下错误,请继续阅读本指南:

Unexpected file accesses building //some/target:label ...
(FileAccessType.READ /path/to/file/not/declared/as/input)

或者,如果您查看的是如下所示的 action()action_foreach() 目标:

action("foo") {
  ...
  hermetic_deps = false
}

构建图表正确性

此 build 被定义为一个有向无环图,其输入流流入操作,输出流从操作流出。例如,某个将 .cc 文件编译为 .o 文件的操作会将源文件作为输入,将目标文件作为输出。编译中使用的任何 .h 头文件都被视为同一操作的输入。

这种图表表示法可确保构建系统可以正确执行增量构建。增量构建是指已经执行了构建,但之后更改了某些操作的输入,现在需要重新构建构建系统。在增量构建中,构建系统会尝试执行最少的工作量,仅重建输入发生变化的操作,无论是由于用户对源代码所做的修改,还是由于需要重新运行的其他操作的输出发生变化。

对于 build 图中的任何操作,都必须列出所有输入和输出,以使 build 图正确无误,并确保操作是封闭的。不过,底层构建系统 Ninja 不会验证这一点。构建操作在用户的本地环境中运行,对整个文件系统(包括源代码树和 out/ 目录中的所有文件)拥有完全访问权限,因此它们未经过沙盒屏蔽,可以访问任何位置。

未声明输入将导致在输入更新时无法重新运行操作(以及下游的所有内容)。如果没有声明输出是另一项操作的输入,则会在相关操作之间产生竞态条件,在这种情况下,单个 build 调用可能会错过时间戳更新,并表现为在单次调用中无法收敛(请参阅 Ninja 空操作)。

如果您正在阅读本文,则说明您可能正在处理一个未完整说明其一个或多个输入或输出的构建操作。

使用自定义操作扩展 build

开发者可以使用 GN 元构建系统在其 BUILD.gn 文件中定义自定义操作。您可以使用 actionaction_foreach 执行此操作。借助自定义操作,开发者可以在构建时调用自定义工具,并将其挂接到依赖关系图,以便在构建时调用这些工具,并在其输入发生更改时针对增量构建正确重新调用这些工具。

操作使用以下参数声明其输入:

  • script:要运行的工具。这通常是 Python 脚本,但也可以是可在主机上执行的任何程序。
  • inputs:用作工具的数据输入的文件。例如,如果该工具压缩文件,则要压缩的文件将列为输入。
  • sources:此项被视为与 inputs 相同。区别只在于语义,因为 sources 通常用于该工具的 script 使用的其他文件,例如相关的 Python 或脚本库。

操作使用以下参数声明其输出:

  • outputs:每个操作必须生成至少一个输出文件。不生成输出文件的操作(例如,验证某些输入是否正确的操作)通常会生成一个“戳记文件”,用作操作已运行且可以为空的指示符。

依赖项

如果在运行操作之前不知道该操作的某些输入,则另外操作可以指定 depfile。依赖项列出了在运行时发现的该操作的一个或多个输出的输入。depfile 的格式为一行或多行,如下所示:

[output_file1] [output_file2...]: [input_file1] [input_file2...]

pfile 中的所有路径都必须相对于 root_build_dir(设为操作的当前工作目录)。另请参阅:首选来自 rebase_path() 的相对路径

编译器等工具应该(并且确实)支持以 depfile 的形式发出编译中使用的所有文件的轨迹。

用于检测非封闭操作的文件系统操作跟踪

Fuchsia 构建系统使用文件系统操作跟踪工具来检测操作是否读取或写入了未明确列为输入或输出的文件(无论是在 BUILD.gn 文件中还是在 depfile 中明确列出),如上所示。这样做是为了代替运行操作的沙盒,以及作为各种运行时排错程序。

如果您正在阅读本页内容,很有可能会遇到此系统错误。错误会准确列出读取或写入的文件,但未在 BUILD.gn 或 depfile 中指定为输入/输出。您应该更正这些遗漏并尝试重新构建,直到错误消失。

为了在本地 build 中重现此错误,您需要确保已启用操作跟踪:

fx set what --args=build_should_trace_actions=true

或者以交互方式运行 fx args,添加一行 build_should_trace_actions=true,保存并退出。

请注意,如果您的操作未以封闭方式定义,并且您没有更正,那么在尝试重新构建操作时,您可能不会遇到错误。由于该操作未以封闭方式定义,因此在增量 build 中可能无法正确获取该操作(这是您尝试解决的问题的一部分)。如需强制运行所有构建操作,您需要先清理构建的输出缓存:

fx clean

默认情况下,CQ 会对所有更改执行这些封闭性检查。它使用上述 build_should_trace_actions=true 参数执行此操作,以便开发者可以在本地重现所跟踪的完全相同的 build。

抑制封闭操作检查

目前非封闭的操作具有以下参数:

action("foo") {
  ...
  # TODO(https://fxbug.dev/xxxxx): delete the line below and fix this
  hermetic_deps = false
}

这样会抑制上述检查。如果您发现有这种抑制行为的操作,则应解除抑制,尝试重现上述问题,然后进行修复。

如果您要提交 bug,而不是立即修复 bug,请将 bug 命名为“[封闭]”,并在说明中添加失败构建操作的轨迹输出。如果您知道违规原因,请就访问违规问题发表评论。

常见问题及其解决方法

缺少输入/输出

有时,某个输入/输出在构建时就众所周知,但只是未指定,或者指定不正确。这些问题常见,并且易于修复。 例如:

输入直到操作运行时才知道

如上所述,有时并非所有输入在构建时都是已知的,因此无法在 BUILD.gn 定义中指定。这就是 depfiles 的用途。

您可以在此处找到有关修复构建操作以生成 depfile 的示例:

输入/输出中缺少操作参数

构建操作通常是以特定文件路径作为参数的脚本。

action("foo") {
  script = "concatenate.py"
  outputs = [ "$target_out_dir/file1_file2.txt" ]
  args = [
    "--concat-from",
    rebase_path("data/file1.txt", root_build_dir),
    rebase_path("data/file2.txt", root_build_dir),
    "--output",
  ] + outputs
}

在上述情况下,您会收到一个操作跟踪程序错误,该错误由 concatenate.pydata/file1.txtdata/file2.txt 读取。错误很容易发现,因为您可以看到这些路径作为参数传递给脚本,但未列为输入或输出。虽然从技术层面来讲,可以将路径作为参数传递,实际上并不能让脚本对这些路径执行读/写操作,但基本上不可能。

解决方法如下:

action("foo") {
  script = "concatenate.py"
  sources = [
    "data/file1.txt",
    "data/file2.txt",
  ]
  outputs = [ "$target_out_dir/file1_file2.txt" ]
  args = [
    "--concat-from",
  ] + rebase_path(sources, root_build_dir) + [
    "--output",
  ] + outputs
}

从文件中扩展参数

有一种常见模式在 Python 脚本中尤为使用,它会将文件的内容扩展为参数(也称为“响应文件”)。在 BUILD.gn 中,您将找到以下内容:

action("foo") {
   script = "myaction.py"
   args = [ "@" + rebase_path(args_file, root_build_dir) ]
   ...
}

然后,在关联的 Python 文件 myaction.py 中,您会看到一个包含 fromfile_prefix_chars 的参数解析器:

def main():
    parser = argparse.ArgumentParser(fromfile_prefix_chars='@')
    args = parser.parse_args()
    ...

上述转换的问题在于,Python 脚本在运行时会读取 args_file,并且应将其指定为输入。要解决此问题,请执行以下操作:

action("foo") {
   script = "myaction.py"
   inputs = [ args_file ]
   args = [ "@" + rebase_path(args_file, root_build_dir) ]
   ...
}

如果您需要从 GN 的列表中快速填充此类文件,可以使用 write_file()

action("foo") {
  args_file = "${target_gen_dir}/${target_name}.args"
  write_file(args_file, a_very_long_list_of_args)
  args = [ "@" + rebase_path(args_file, root_build_dir) ]
  ...
}

请注意,出于此目的,GN 提供了 response_file_contents(而不是 write_file)作为便捷的替代方案。不过,由于 Ninja 存在一个 bug,我们目前不允许在 build 中使用 response_file_contents

创建和删除临时文件

创建临时文件是构建操作中的常见模式。请勿将临时文件列为输出,只要创建临时文件的操作也在返回之前删除这些文件即可。

临时文件应保存在 target_out_dirtarget_gen_dir 下。不建议使用全局临时存储空间(例如 /tmp$TMPDIR),或者在检出目录或输出目录之外执行任何读写操作,因为这可能会使构建失败问题排查更加困难,因为可能需要从文件系统中的其他位置恢复文件以指明问题所在。

创建和删除临时目录

有时,您需要在临时目录中创建临时文件。同样,这没有问题,前提是创建临时目录的操作在返回之前也以递归方式将其删除。

shutil.rmtree 是用于删除临时目录的常用函数。但是,由于我们跟踪器的限制,这有时会导致虚假的意外读取。另请参阅:问题 75057:在操作跟踪程序中正确处理通过 shil.rmtree 删除的目录

若要绕过此限制,一种方法是仅创建临时文件,而不是创建临时目录。临时文件应写入 target_out_dirtarget_gen_dir 下。

有时无法做到这一点,例如,临时目录由无法修改的外部构建工具创建时。在这种情况下,另一种方法是为临时目录指定一个特殊名称(例如 __untraced_foo_tmp_outputs__),并在操作跟踪器中将其列入许可名单。对这个特殊目录中的文件进行的访问将被跟踪程序忽略。因此,请勿轻易使用此功能。

例如,假设 bar.py 始终删除传递给它的 --tmp-dir 中的所有文件,然后重新填充:

action(target_name) {
  script = "bar.py"
  args = [
    "--tmp-dir"
    rebase_path("${target_gen_dir}/${target_name}/__untraced_bar_tmp_outputs__", root_build_dir)
  ]
  ...
}

然后,在操作跟踪器的 ignored_path_parts 中添加一个条目:

ignored_path_parts = {
  # Comment with clear explanation on why this is necessary,
  # preferably with a link to an associated bug for more context.
  "__untraced_bar_tmp_outputs__",
  ...
}

CQ 中报告的无法在本地重现的错误

首先,请确保您使用的是 build 参数 build_should_trace_actions=true,如上所述。

如果 CQ 报告某个 Python 文件被 action_tracer.py 意外读取,但您无法在本地重现此问题,则原因可能是已编译的 Python 文件缓存在整个树中的 __pycache__ 目录(例如 find third_party -type d -name __pycache__)中。快速解决方法是删除这些目录中的所有 *.pyc 文件。出现此假负例的原因是文件系统从不打开原始 .py 文件,因此不会报告为已轻触该文件,因此不会触发失败的封闭性检查。

Python 以外的文件类型也可能因类似原因而无法重现。

另请参阅:开放项目中的封闭操作