RFC-0119:系统绝对路径被视为有害

RFC-0119:系统绝对路径被视为有害
状态已接受
区域
  • 构建
说明

实施一项政策,尽可能在所有位置使用相对路径。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-06-20
审核日期(年-月-日)2021-07-28

摘要

  1. 制定一项政策,在多个列举的实例中优先使用相对路径或源绝对路径,而不是系统绝对路径,并着眼于未来的使用情形。
  2. 以自动方式强制执行此政策,建立回归停止。
  3. 清理了之前对系统绝对路径的使用。

背景

熟悉该主题的读者可能希望跳过

路径

定义

读者应熟悉路径的概念。

下面我们使用以下定义:

  • 系统绝对路径:以本地文件系统为根的路径。通常带有 / 前缀。
  • 源绝对路径或项目绝对路径:相对于源树或项目签出的根的路径。通常以 // 前缀表示。
  • 相对于 CWD 的路径:相对于当前工作目录的路径。

下面,我们将系统绝对路径统称为绝对路径,将其他路径统称为相对路径,因为它们是相对于另一条路径而言的。

Fuchsia 构建系统中的路径

路径用于在 build 系统中引用操作的输入和输出文件。Fuchsia 构建系统使用 GN 来定义其构建图。既定的最佳实践是,最好将 GN 中的路径表示为相对于另一个根目录(例如根 build 目录或源根目录)的相对路径。

生成代码中的路径

代码可能会因各种原因而引用路径。签入的源代码不能引用绝对路径,因为它们不具备可移植性 - 在提交队列 (CQ) 机器上没有意义,会被拒绝;即使有意义,当其他工程师签出相同来源时,相同的绝对路径在他们的机器上也没有意义。不过,在特定机器上生成且未签入的代码可能包含绝对路径,但仍可正常运行(成功构建和/或运行)。

Fuchsia 利用多种工具来生成源代码。例如,FIDL 使用 fidlc,而 Banjo 使用类似的工具。

设计初衷

在 build 系统、工具调用、生成的源代码和其他制品中,相对路径比绝对路径更受欢迎,原因有以下几点。

可移植的制品

如上所述,相对路径可以移植,因为它们是相对于双方可以达成一致的参考点(例如 Fuchsia 源代码检出的根目录或 build 输出目录的根目录)而言的。在许多情况下,路径不仅最好是可移植的,而且必须是可移植的。

用于分布式 build 的可移植工件

分发构建操作是指将构建操作(例如将 C/C++ 文件编译为对象文件)的输入发送到远程服务器以供执行。远程服务器可能会代表客户端执行操作并返回结果。远程服务器通常会依赖基于内容的缓存来完全跳过该操作。

分发 build 操作有很多好处,但不在本文档的讨论范围内。

由于客户端和服务器可能在绝对路径上存在分歧,因此在指定要分发的调用的详细信息以及任何上传的工件的内容时,可能需要使用相对路径,因为它们会相互引用。对于依赖于缓存的分布式 build 系统,这一点尤其重要,因为调用详细信息会用作缓存键的一部分。即使允许使用绝对路径,使用绝对路径也可能会破坏缓存机制,因为两个客户端可能会签出相同的源代码,但随后向服务器发送的请求在绝对路径方面有所不同。

之前,build 路径或生成的代码中存在绝对路径导致了分布式 build 方面的问题。例如,Fuchsia 开发者可以使用 Goma 来分发 C/C++ 构建操作。之前,当引入通过 C/C++ include 目录使用绝对路径的更改时,Goma 的 Fuchsia 用户曾遇到过服务中断问题。在某些情况下,分布式 build 会失败并强制执行本地回退,这会降低速度。在其他实例中,分布式 build 会成功,但无法命中缓存,导致后端负载增加一个数量级,从而导致级联故障。

在分发 build 操作时,另一种类似的故障模式是工具调用中的绝对路径。我们之前发现,在分发操作之前检查此类路径非常有用,并且可以以 build 操作失败和有用的错误的形式拒绝此类路径。

其他实例中,绝对路径导致了 build 正确性问题。

用于流水线的可移植制品

在分配操作时,有时需要为不同的任务设置远程服务器流水线。例如,有些机器可能更适合运行 build,而另一些机器更适合运行已构建的测试。有时,操作会表示为分叉和联接的图,例如构建一套测试,然后分叉到多台机器,每台机器运行一部分测试,然后联接结果。

在之前的示例中,路径是在客户端和服务器之间交换的,而在本示例中,路径是在流水线中的不同服务器之间交换的。尽管交换的性质不同,但出于相同的原因,相对路径是首选。

绝对路径可能会导致流水线中断。例如,生成测试覆盖率的流水线过去曾出现中断,原因是较早阶段生成的覆盖率报告包含源代码的绝对路径,而这些路径在运行覆盖率报告生成流水线后期阶段的不同服务器中无法解析。生成覆盖率映射文件的工具使用了绝对路径,我们更改了这些路径,使其相对于给定的基础路径。

使用另一种形式的代码插桩时也发生了类似的破坏 - 调试信息文件中曾经使用绝对路径,这些记录用于将相对 PC 偏移量解析为源代码行。

用于缓存的可移植制品

构建输出可能会被缓存,以加快后续构建(称为增量构建)。此类缓存通常保存在本地,例如开发者的工作站上或构建服务器的特定实例上。从理论上讲,不同的 build 工作器(工作站和服务器)之间也可以交换 build 缓存,前提是网络容量允许这样做,并且不存在安全和隐私问题。

Fuchsia 目前不会在不同机器之间重复使用 build 缓存,因为已知某些 build 输出包含绝对制品。相反,Fuchsia 开发者和 Fuchsia 分布式构建器最多会使用他们之前运行的构建中的本地化缓存。这会错失大量优化机会,并降低工程效率。

可重现的制品

在制品中使用绝对路径会妨碍我们实现可重现的 build

目前,可重现 build 并不是 Fuchsia 的既定目标。不过,考虑到可重现 build 在未来可能成为理想的属性,并且工件中使用绝对路径会妨碍我们实现可重现性,因此有必要考虑可重现 build 的优势。

另请注意,还有其他来源可能会导致制品无法重现,最常见的是时间戳,但这些来源不在本 RFC 的范围内。

工作量极少

Fuchsia 目前通过生成完整的系统映像并铺平设备来在设备上运行测试。未来,我们可能希望加快此流程,例如仅将自上次更新以来发生变化的 blob 推送到测试设备。预计大多数要测试的更改只会影响少量 blob(相对于其基本更改),因此这种操作方法可以显著加快测试设备的启动速度。

如果绝对路径泄露到制品中,那么在要测试的不同更改之间,可能会有比绝对需要更多的 blob 失效。

树外 Fuchsia build

上文介绍了 Fuchsia 因绝对路径而遇到的一些问题,以及绝对路径在哪些方面使 Fuchsia 难以发展和改进。解决绝对路径问题的紧迫性取决于历史背景。例如,从历史上看,Fuchsia 并未利用增量 build 或缓存,因此该项目及其相关人员学会了容忍阻碍 Fuchsia 采用更多增量 build 和缓存的缺陷。

如果 Fuchsia 取得成功,那么其他项目将使用 Fuchsia 的代码和制品,并为 Fuchsia 进行开发。可以放心地假设,至少有一些项目会需要一套比 Fuchsia 项目更符合不同需求的 build 规则和工具系统。例如,其中一些客户的运营规模可能非常大,必须使用增量构建缓存也是如此。如果 Fuchsia 在实现这些属性方面存在障碍,那么 Fuchsia 开发者和其他客户在采用 Fuchsia 时就会遇到障碍。

分布式信任

如果 build 的所有制品都是可重现的,那么这会为 build 系统带来新的属性。例如,可重现的 build 可以作为分布式替代方案,用于验证分布式二进制文件的完整性,以取代加密信任链。不信任方只需尝试从相同的来源和构建系统重现这些二进制文件,即可审核它们。无法重现二进制文件可能是恶意篡改的证据。

如果制品中使用绝对路径,则不信任方将永远无法重现相同的结果。

便捷性

在排查问题时,相对路径更易于使用。人们通常更期望获得一致性,因此可以比较成功操作和不成功操作之间的路径,并发现任何有意义的差异。

如果仅在本地工作,绝对路径会非常方便。例如,绝对路径可以从工具调用中复制,并在不同的本地 shell 环境中使用,并且保证可以正常运行,因为它不依赖于当前工作目录。此外,任何两个绝对路径和规范化路径(例如,... 部分已解析、链接已跟踪等)都可以通过字符串标识来检查是否等效。由于任何路径都可以转换为绝对路径并进行规范化,并且这种转换是幂等的,因此这提供了一种在本地环境中对路径进行简单等效性检查的方法。不过,如果用户觉得更方便,可以对绝对路径执行此转换,但没有任何限制。不过,由于从相对形式到绝对形式和/或规范化形式的转换是破坏性的,因此无法反向执行。因此,为了涵盖所有使用情形,最好使用相对形式。

设计

政策

我们将把已记录的 GN 最佳实践提升为一般政策,并将其应用范围扩大到 BUILD.gn 文件以外的范围。具体而言,我们将建议您执行以下操作:

  1. 由 build 系统作为命令行实参传递给工具的路径应相对于调用工具的当前工作目录(对于 GN/Ninja,该目录表示为 root_build_dir)。
  2. 在构建时生成的文件中的路径应相对于同一根构建目录。例如:生成的源代码、软件包清单、depfiles
  3. 在运行时生成的路径应相对于项目源根目录。例如:崩溃中的文件信息、测试覆盖率报告。

违规处置

我们将推出新工具,以针对工具调用和制品中的绝对路径来清理 Fuchsia build。这些工具将在 CQ 中运行,以防止出现回归。

清理

上述工具将提供许可名单,该名单会初始化为列出所有现有的政策违规行为。我们将启动清理工作,将许可名单的大小减小为零。在正常情况下,回归不会被添加到许可名单中。

实现

强制执行工具的运作方式的实现细节未达到 RFC 的级别。不过,为了满足好奇的读者,下面提供了一些创意草图。

清理 Ninja 文件

运行 GN 会生成一个描述 build 图的 build.ninja 文件。此图的说明包含所有工具调用,包括要调用的工具的路径以及作为实参传递给这些工具的路径。使用的其他文件在 depfile 中引用。

可以使用 strings 处理这些文件,生成可过滤的令牌,使其看起来像是绝对路径。例如,此简单扫描器可作为主机测试来实现,该测试可根据所有 build 变体运行。

清理 build 引用的文件

除了清理 Ninja 文件之外,我们还可以对指定为 build 操作的输入或输出的任何文件进行令牌化和清理。假设 build 是密封的,我们就能从 Ninja 图或 depfile 中发现所有此类文件。

检查是否存在绝对路径字符串

我们可以扫描 build 输出目录 (out/) 下的所有文件,生成字符串,检查是否有任何字符串是绝对路径,然后发出错误。它并非万无一失的保护措施,而是一道额外的简单防线。

拒绝操作轨迹记录器中的绝对路径

我们已经有了一个封装 GN 操作的工具,并且已经使用该工具来拒绝 depfile 中的绝对路径。我们可以进一步扩展此机制。

请注意,我们目前仅使用操作跟踪器来封装自定义操作,而自定义操作只是所有 build 操作的一个子集。我们这样做是出于上述性能方面的考虑。从这个角度来看,或许进一步依赖操作跟踪并不是一种稳健全面的策略。

使绝对路径失效

一种简单的方法是,在完全使绝对路径失效的同时,保持签出中的所有相对路径处于正常工作状态,具体做法是生成 Ninja,然后将签出目录移至其他位置(或只是重命名),然后进行构建。

$ fx gen
$ mv $FUCHSIA_DIR ${FUCHSIA_DIR}_renamed
$ fx build

如果任何 build 调用将签出下的路径引用为绝对路径,则 build 将失败。

此方法非常易于实现,具有可移植性,并且不会产生性能开销。一些缺点包括,如果出现中断,错误消息会让新手感到困惑,并且仍有可能在生成的制品(例如 depfile、srcgen、调试信息)中泄露绝对路径。

对运行时环境的更改

另一种方法是更改 build 的运行时环境,使绝对路径无效或无害。例如,某些项目使用 chroot 等概念在结账根目录中形成沙盒。其他构建系统使用特殊的文件系统来实现沙盒化。

运行时方法值得考虑,因为它们可以提供更强的正确性保证。不过,仍需考虑性能问题和具有挑战性的可移植性 问题

安全注意事项

路径可用于以下位置:路径的解释(解析为哪个实际文件)可能会影响敏感的系统行为。例如,可映射到内存中作为可执行页面的文件许可名单。

在这种情况下,最好使用项目相关路径,而不是相对于包含指定列表的目录的路径,或相对于处理此列表的工具的 CWD 的路径。这是因为项目相关路径的解析在定义它们的项目中是明确的,而其他形式的相对路径可能会根据全局可变状态(例如 CWD)以不同的方式进行解释。

隐私注意事项

绝对路径有时可能会泄露个人身份信息。例如,用户的用户名通常会出现在包含该用户签出或构建输出目录中文件的绝对路径中。将绝对路径替换为源相对路径可消除此 PII 输出口。

测试

上文中,我们探讨了一些实现检查的方法,这些检查可以捕获绝对路径的使用情况。由于这些检查是作为 build 步骤或主机测试实现的,因此可以在 CQ 中运行并充当持续测试。

确保不使用绝对路径的另一种方法是,让它们的存在对工程工作流的关键方面造成不可容忍的影响。例如,如果绝对路径会破坏某个已分发的操作,而该操作是 CQ 的一部分,那么开发者就无法再引入破坏性更改。

文档

当强制执行路径不是绝对路径的工具失败时,应生成一个链接到相应问题排查页面的错误。举例来说,当强制执行 build 操作的操作跟踪器失败时,它会生成一条错误消息,其中包含指向密封 build 操作页面的链接。

缺点、替代方案和未知因素

如果我们什么都不做,就会错失分布式构建领域和可重现性领域的机遇。

我们可以制定政策,但无法强制执行。可能造成的后果是,无法在分布式 build 或可重现性方面取得有意义的进展。

我们可以暂时搁置此问题,但代价是随着时间的推移,会引入更多回归,这是熵衰减的本质。

在先技术和参考资料

一位伟大的绝地武士曾说过:“只有西斯才会使用绝对的字眼。”