构建图收敛

构建图收敛是指单个构建调用将以正确的顺序执行所有必要的操作,以便每个操作的输出比输入新

Fuchsia 使用 Ninja 构建系统,该系统由时间戳驱动。Ninja 将 build 表示为输入/输出文件以及接受输入并生成输出的操作的图表。

当您运行 build(例如使用 fx build)时,Ninja 将遍历 build 图,并执行输出不存在或输入自上次运行以来发生了更改的任何操作,所有操作均按拓扑顺序(依赖项之前的依赖项)执行。

但是,构建图操作未经验证,无法实现输出比输入新这一承诺,这可能会导致收敛问题。

常见根本原因

造成 Ninja 收敛问题的方法有很多。不过,先前的经验告诉我们,这些问题有常见的根本原因。

未生成输出

如果声明构建操作会生成输出,但实际上并不(在某些情况下或曾经)生成该输出,则会导致收敛问题。例如,某项操作可能会声明成功生成图章文件,但无法生成或轻触此图章文件,或将文件保存到错误的位置。

输出已过时(不比所有输入新)

如果输出比所有输入新,Ninja 就会知道该输出是最新的。如果一个或多个输入在输出保存后发生了变化,那么 Ninja 将重复生成输出所需的步骤。

但是,如果生成输出的操作在输入发生更改时未更新输出,则会导致出现永久过时状态的外观。

导致这种情况的一个常见错误是,操作会审核输入,认为操作对其输出的内容无任何操作/更改,但无法更新输出的修改时间戳(即,对其输出进行“轻触”或“时间戳”)。

修改输入

操作可以修改其输入。通常,操作的输入应仅以读取权限打开,但是写入这些内容也没什么问题。也就是说,如果您的操作需要修改输入,则应在写入任何输出之前进行修改。或者,如果您必须在写入输出后修改输入,请务必在退出操作之前更新输出的时间戳。否则,您将更新一个或多个输入,使其比一个或多个输出更新,从而让忍者误以为输出已过时。

修改操作中的输入还可能引入竞态条件,导致问题的重现具有不确定性。如果多项操作依赖于同一输入,并且其中一项修改了输入,则当其中一项操作导致的输入时间戳晚于任何操作的输出时,构建都将无法收敛。在依赖项排序的执行中,无法保证独立操作的相对顺序。

避免修改输入。

Ninja 构建系统通过符号链接来确定时间戳。当软符号链接作为输入依赖项或输出参与 Ninja 规则时,这可能会带来意想不到的后果。符号链接本身的时间戳(而不是其目的地)的时间戳不会考虑过时和新鲜度。有关 stat()lstat() 的说明以及演示,请参阅 ninja#1186。硬链接(不带 -sln)存在以下问题:多个引用指向同一个文件系统对象,因此具有相同的时间戳。

即使是简单的链接操作也可能会导致问题。假设有一个简单的操作,其中输入 src、输出 $target_out_dir/dst 且调用的操作为 ln src $target_out_dir/dst。从表面来看,这项操作会正确收敛。不过,action() 的行为可能会在构建系统中的其他位置被替换,例如用其他操作封装操作。因此,当 src 的时间戳早于封装容器操作的脚本时,您的内部操作可能不会收敛,然后系统会认为该时间戳早于 dst(其输出,其中包含输入的时间戳)。copy() 不会遇到同样的问题,因为它从未封装。

避免操作输入和输出中的符号链接和硬链接。

如需制作副本,请优先使用内置的 copy() 目标。

时间戳粒度

现代文件系统以纳秒为单位存储文件上的时间戳(例如上次修改的时间)。某些较旧的运行时(如 Python 2.7)会以较低的分辨率(例如毫秒)保留文件时间戳。因此,如果输入和输出的写入时间为同一毫秒,且输出的时间戳在毫秒数位后截断,则操作读取输入和写入输出时,其时间戳被视为“现在”,但实际早于输入的时间戳。

在撰写本文时,我们部署了相关机制来确保构建中的所有 Python 操作均使用 Python 3.x 运行,这在一定程度上是为了避免这个问题。

构建收敛诊断

我们使用以下工具诊断构建收敛问题:

  • 提交队列中的 Ninja 空操作检查
  • 文件系统访问操作跟踪

忍者空操作检查

Fuchsia 的提交队列 (CQ) 可验证更改不仅会成功构建,还会使构建系统处于在单次构建调用中收敛到空操作的状态。

CQ 中的构建收敛错误示例:

fuchsia confirm no-op
ninja build does not converge to a no-op

同一 build 会先在 CQ 中运行,然后再将更改合并到源代码树中,以确保更改不会破坏 build。成功完成构建后,CQ 将再次调用 Ninja,并要求 Ninja 报告 "no work to do"。这起到健全性检查的作用,因为正确的 build 图应该能够“收敛”为空操作。

如果此可靠性检查失败,CQ 将在名为 fuchsia confirm no-op 的步骤中报告失败。

重现 Ninja 融合问题

源代码树已同步到您所做的更改后,只需尝试执行以下操作即可:

fx build

此命令应输出以下内容:

ninja: no work to do.

如果情况并非如此,并且系统正在执行实际的构建操作,请再次运行同一命令。如果第二次调用仍未产生“无工作”,则表示您重现了该问题。如果仍然看到“无工作”状态,请尝试以下操作:

# Clean your build cache
rm -rf out
# Set up the build specification again
fx set ...
# Build
fx build
# Build again, expecting no-op
fx build

排查 Ninja 收敛问题

在 CQ 结果页面的失败步骤 confirm no-op 下,您将看到多个链接:

  • 执行详情
  • 忍者 -d 解释 -n -v
  • 脏路径

指向 ninja -d explain -n -v 的链接显示了您应该能够使用以下命令在本地重现的信息:

fx ninja -C $(fx get-build-dir) -d explain -n -v

指向“脏路径”的链接会显示相同信息中最相关的子集。您将看到一个最有可能按以下格式开头的文本文件:

ninja explain: output <...> doesn't exist
...

此文件中的每一行都像一块多米诺骨牌。排查问题时,您应该先查看第一块多米诺骨牌,它引发额外工作的链式反应。例如,在上面的示例中,某个输出文件不存在,这会导致 Ninja 重新运行本应生成此输出的构建操作,然后重新运行依赖操作。

文件系统访问跟踪

还有用于跟踪操作的文件系统访问的构建器。过时或缺失的输出的诊断如下所示:

Not all outputs of //your:label were written or touched, which can cause subsequent
build invocations to re-execute actions due to a missing file or old timestamp.

Required writes:
...

Missing outputs:
...

Stale outputs:
...

不允许写入输入的诊断如下所示:

Unexpected file accesses building //your/target:label, following the order they are accessed:
(FileAccessType.WRITE /path/to/input-that-should-not-be-touched.txt)

与 Ninja 空操作检查相比,这种检查会针对每项操作执行,并在操作发生时立即诊断导致收敛问题的众多原因之一,而不是稍后通过完整的 fx build 命令进行诊断。这种方法可以发现一些因竞态条件而难以重现的问题。

排查所跟踪操作失败问题

如需在本地启用操作跟踪,请执行以下操作之一:

  • 运行 fx set ... --args=build_should_trace_actions=true
  • 运行 fx args,在编辑器中添加 build_should_trace_actions=true,然后保存并退出

然后是 fx build //your/failing:target

在操作的脚本或命令上下文中检查消息中的文件,看看它们是否属于某个常见问题类别。