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

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

制定政策以尽可能在所有地方使用相对路径。

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

总结

  1. 在多个枚举实例中制定优先使用相对路径或来源绝对路径而非系统绝对路径的政策,并着眼于未来的用例。
  2. 移动以自动方式强制执行此政策,从而停止回归。
  3. 清理之前使用的系统绝对路径。

背景

熟悉此主题的读者可以跳过

路径

定义

读者应熟悉路径的概念。

下面,我们使用以下定义:

  • 系统绝对路径:位于本地文件系统的根路径。通常用 / 前缀表示。
  • 源代码绝对路径或项目绝对路径:相对于源代码树或项目检出的根目录的路径。通常用 // 前缀表示。
  • CWD 相对路径:相对于当前工作目录的路径。

在下文中,我们将系统绝对路径称为绝对路径,并将其他路径称为相对路径,因为它们是以相对于另一个路径的相对方式表示的。

Fuchsia 构建系统中的路径

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

已生成代码中的路径

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

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

设计初衷

在构建系统、工具调用、生成的源代码和其他工件中,最好使用相对路径而非绝对路径。

可移植的工件

如上所述,相对路径是可移植的,因为它们相对于双方可以商定的参考点,例如 Fuchsia 源检出的根目录或 build 输出目录的根目录。在许多情况下,不仅路径具有可移植性,这也是一项要求。

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

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

分发构建操作具有许多优势,但这些优势不在本文档的讨论范围内。

由于客户端和服务器可能无法就绝对路径达成一致,因此,在指定要分发的调用的详细信息时,以及在上传工件的内容中相互引用时,可能需要使用相对路径。对于依赖缓存的分布式构建系统来说尤其如此,在此类系统中,调用详细信息会用作缓存键的一部分。即使允许使用绝对路径,它们的使用也可能会使缓存机制失效,因为两个客户端可能会检出相同的源代码,但随后会向绝对路径不同的服务器发送请求。

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

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

在其他情况下,绝对路径会导致构建正确性问题。

用于流水线的可移植工件

在分发操作时,有时需要针对不同任务提供远程服务器流水线。例如,一些机器可能更适合运行 build,而其他机器可能更适合运行已构建的测试。有时,操作以可分叉和联接的图来表示,例如,构建一套测试,然后分支到多台机器,每个机器运行一个测试分片,然后联接结果。

在前面的示例中,路径在客户端和服务器之间交换,而在这种情况下,路径在流水线中的不同服务器之间交换。尽管交换的性质不同,但出于同样的原因,我们首选相对路径。

绝对路径可能会导致流水线中断。例如,过去生成测试覆盖率的流水线中断了,前一阶段生成的覆盖率报告包含源代码的绝对路径,但后来运行覆盖率报告生成流水线后期阶段的不同服务器中无法解析这些路径。生成覆盖率映射文件的工具使用了绝对路径,我们已更改绝对路径,以将路径相对到给定基准路径。

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

用于缓存的可移植工件

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

Fuchsia 目前不会在不同机器之间重复使用构建缓存,因为已知某些构建输出包含绝对工件。相反,Fuchsia 开发者和 Fuchsia 分布式构建器最多只会使用自己运行的先前 build 中的本地化缓存。这是一个重大失去优化和提高工程生产力的机会。

可重现的工件

在工件中使用绝对路径会导致我们无法获得可重现的 build

目前,可重现的 build 并不是 Fuchsia 的既定目标。不过,我们有必要考虑一下可重现 build 的优势,因为我们了解这可能是未来的预期属性,并且在工件中使用绝对路径会导致我们无法实现可重现性。

另请注意,还有一些工件不可再现性的其他来源(最常见的是时间戳),不在本 RFC 的讨论范围之内。

最少工作量

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

如果绝对路径泄露到工件中,则在要测试的不同更改之间失效的 blob 可能会比绝对所需的多很多。

树外 Fuchsia build

在上文中,我们介绍了 Fuchsia 因绝对路径而遇到的一些问题,以及绝对路径使 Fuchsia 难以进化和改进的一些方式。解决具有绝对路径的问题的紧迫性取决于历史背景。例如,Fuchsia 过去没有利用增量 build 或缓存,因此项目和参与其中的人员学会容忍那些使 Fuchsia 无法采用更多增量 build 和缓存的缺陷。

如果 Fuchsia 成功,其他项目将使用 Fuchsia 的代码和工件,并为 Fuchsia 开发项目。可以放心地假设其中至少有一部分项目需要一个构建规则和工具系统,这种系统比 Fuchsia 项目的不同需求更适合。例如,其中一些客户可能以大规模的方式运营,其中增量构建是必需的缓存也是。如果 Fuchsia 为实现这些属性提供了障碍,则 Fuchsia 开发者和其他客户将在采用方面遇到障碍。

分布式信任

如果 build 的所有工件都可重现,这为构建系统打开了新属性之门。例如,可重现的 build 可以充当加密信任链的分布式替代方案,以验证分布式二进制文件的完整性。不可信方只需尝试从相同的源代码和构建系统重现这些二进制文件即可审核这些二进制文件。如果未能重现二进制文件,则可能是恶意篡改的证据。

如果在工件中使用绝对路径,不可信方将永远无法重现相同的结果。

便利性

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

如果严格在本地工作,绝对路径会非常方便。例如,您可以从工具调用中复制绝对路径,并在其他本地 shell 环境中使用它,并且绝对路径能够保证正常运行,因为它对于实例而言对当前工作目录不敏感。此外,还可以通过字符串标识检查任何绝对和标准化路径(例如,解析 ... 部分、跟踪链接等等)的任何两个路径是否等同。由于任何路径都可以设为绝对路径和标准化路径,并且这种转换具有幂等性,因此可针对本地环境中的路径提供简单的等价性检查。不过,如果用户觉得在绝对路径上执行这种转换更方便,则没有任何限制,但是由于这种从相对绝对和/或标准化形式的转换具有破坏性,因此无法在另一个方向上执行。因此,它更全面地涵盖所有用例,倾向于选择相对形式。

设计

政策

我们会将载明的 GN 最佳实践推广为一般政策,并将其运用范围扩大到 BUILD.gn 文件。 具体来说,我们将提供以下建议:

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

违规处置

我们将引入一些新工具,以根据工具调用和工件中是否存在绝对路径对 Fuchsia build 进行排错。这些工具将在 CQ 中运行,以防止出现回归。

清理

上述工具将提供许可名单,该许可名单将进行初始化以列出所有现有违反政策的行为。系统将启动清理工作,将许可名单的大小缩减为零。正常情况下,回归不允许进入许可名单。

实现

强制执行工具如何运行的实现细节没有上升到 RFC 级别。不过,我们在下面提供了一些思路草图,以便感兴趣的读者参考。

清理 Ninja 文件

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

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

对 build 引用的文件进行排错

除了清理 Ninja 文件之外,我们还可以对指定为构建操作输入或输出的任何文件进行标记化和清理。如果 build 是封闭的,我们将能够从 Ninja 图或 depfile 中发现所有此类文件。

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

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

在操作跟踪器中拒绝绝对路径

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

请注意,我们目前仅使用操作跟踪器来封装自定义操作,这些操作是所有构建操作的一部分。我们这样做不是为了解决上述性能问题。从这个角度来看,进一步依靠动作跟踪并不是一个可靠的全面策略。

使绝对路径失效

若要使结账中的所有相对路径保持有效状态,同时使绝对路径完全无效,一种简单的方法是生成 Ninja,然后将结账目录移至其他位置(或直接对其重命名),再进行构建。

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

如果任何构建调用将检出路径下引用为绝对路径,则构建将失败。

这种方法实现起来非常简单、可移植,并且不会产生性能开销。这样做的缺点包括,如果出现故障,显示错误消息会让非发起者感到困惑,并且仍然可能会泄露生成的工件中的绝对路径(例如 depfiles、srcgen、debuginfo)。

对运行时环境的更改

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

运行时方法值得考虑,因为它们可以带来更强的正确性保证。但是,您需要考虑性能顾虑和具有挑战性的可移植性 问题

安全注意事项

如果路径的解释(它被解析到的实际文件)可能会影响敏感系统行为,那么就可以在这些位置使用路径。例如,可以作为可执行页面映射到内存的文件许可名单。

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

隐私注意事项

绝对路径偶尔可能会泄露个人身份信息。例如,用户的用户名通常是包含其检出或构建输出目录中的文件的绝对路径的一部分。将绝对路径替换为来源相对路径可以消除 PII 的这个出口。

测试

在上文中,我们探索了执行可捕获绝对路径使用的一些方式。由于这些检查是作为构建步骤或主机测试实现的,因此它们可以在 CQ 中运行,并充当连续测试。

确保不使用绝对路径的另一种方法是,使工程工作流的关键环节无法容忍绝对路径的存在。例如,如果绝对路径破坏了某个分布式操作,并且该操作是 CQ 的一部分,那么开发者就不能再引入破坏问题。

文档

当强制实施该路径的工具并非绝对失败时,它们应产生一个链接到相应问题排查页面的错误。作为灵感,当强制构建操作为封闭构建操作的操作跟踪器失败时,它会生成一条错误消息,其中包含指向封闭构建操作页面的链接。

缺点、替代方案和未知情况

在分布式构建空间和可再现性空间中,我们无法采取任何措施,让您错失良机。

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

我们可以规避此问题,代价是随着时间的推移会产生额外的回归问题,熵衰减的性质也是如此。

早期技术和参考资料

一位伟大的绝地武士曾说过:“只有西斯才有绝对价值。”