构建图收敛是指单个构建调用将以正确的顺序执行所有必要的操作,以便每个操作的输出比输入新。
Fuchsia 使用 Ninja 构建系统,该系统由时间戳驱动。Ninja 将 build 表示为输入/输出文件以及接受输入并生成输出的操作的图表。
当您运行 build(例如使用 fx build
)时,Ninja 将遍历 build 图,并执行输出不存在或输入自上次运行以来发生了更改的任何操作,所有操作均按拓扑顺序(依赖项之前的依赖项)执行。
但是,构建图操作未经验证,无法实现输出比输入新这一承诺,这可能会导致收敛问题。
常见根本原因
造成 Ninja 收敛问题的方法有很多。不过,先前的经验告诉我们,这些问题有常见的根本原因。
未生成输出
如果声明构建操作会生成输出,但实际上并不(在某些情况下或曾经)生成该输出,则会导致收敛问题。例如,某项操作可能会声明成功生成图章文件,但无法生成或轻触此图章文件,或将文件保存到错误的位置。
输出已过时(不比所有输入新)
如果输出比所有输入新,Ninja 就会知道该输出是最新的。如果一个或多个输入在输出保存后发生了变化,那么 Ninja 将重复生成输出所需的步骤。
但是,如果生成输出的操作在输入发生更改时未更新输出,则会导致出现永久过时状态的外观。
导致这种情况的一个常见错误是,操作会审核输入,认为操作对其输出的内容无任何操作/更改,但无法更新输出的修改时间戳(即,对其输出进行“轻触”或“时间戳”)。
修改输入
操作可以修改其输入。通常,操作的输入应仅以读取权限打开,但是写入这些内容也没什么问题。也就是说,如果您的操作需要修改输入,则应在写入任何输出之前进行修改。或者,如果您必须在写入输出后修改输入,请务必在退出操作之前更新输出的时间戳。否则,您将更新一个或多个输入,使其比一个或多个输出更新,从而让忍者误以为输出已过时。
修改操作中的输入还可能引入竞态条件,导致问题的重现具有不确定性。如果多项操作依赖于同一输入,并且其中一项修改了输入,则当其中一项操作导致的输入时间戳晚于任何操作的输出时,构建都将无法收敛。在依赖项排序的执行中,无法保证独立操作的相对顺序。
避免修改输入。
符号链接和硬链接
Ninja 构建系统通过符号链接来确定时间戳。当软符号链接作为输入依赖项或输出参与 Ninja 规则时,这可能会带来意想不到的后果。符号链接本身的时间戳(而不是其目的地)的时间戳不会考虑过时和新鲜度。有关 stat()
和 lstat()
的说明以及演示,请参阅 ninja#1186。硬链接(不带 -s
的 ln
)存在以下问题:多个引用指向同一个文件系统对象,因此具有相同的时间戳。
即使是简单的链接操作也可能会导致问题。假设有一个简单的操作,其中输入 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
。
在操作的脚本或命令上下文中检查消息中的文件,看看它们是否属于某个常见问题类别。