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

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

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

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

摘要

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

背景

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

路径

定义

读者应熟悉 路径的概念。

下面我们使用以下定义:

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

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

Fuchsia 构建系统中的路径

路径用于构建系统中,以引用操作的输入和输出文件。 Fuchsia 构建系统使用 GN 来定义其构建 图。有一种既定的最佳实践,即优先 在 GN 中使用相对于另一个根目录(例如 根构建目录或源根目录)的路径。

生成代码中的路径

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

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

设计初衷

在构建系统、工具调用、生成的源代码和其他工件中,优先使用相对路径而不是绝对路径有几个原因。

可移植的工件

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

用于分布式构建的可移植工件

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

分发构建操作有很多好处,这些好处不在本文档的讨论范围内。

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

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

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

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

用于流水线处理的可移植工件

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

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

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

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

用于缓存的可移植工件

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

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

可重现的工件

在工件中使用绝对路径会阻止我们实现可重现的 构建

目前,可重现的构建不是 Fuchsia 的既定目标。不过,考虑到可重现的构建在未来可能是一项理想的属性,并且在制品中使用绝对路径会阻止我们实现可重现性,因此考虑可重现的构建的优势很有意义。

另请注意,还有其他来源会导致工件不可重现,最常见的是时间戳,这些来源不在本 RFC 的讨论范围内。

最少的工作量

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

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

树外 Fuchsia 构建

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

如果 Fuchsia 取得成功,那么其他项目将使用 Fuchsia 的代码和工件,并为 Fuchsia 开发。可以肯定的是,至少其中一些项目将期望构建规则和工具系统比 Fuchsia 项目的系统更能满足不同的需求。例如,其中一些客户可能以如此大的规模运营,以至于增量构建 和缓存都是必需的。如果 Fuchsia 阻碍了实现这些属性,那么 Fuchsia 开发者和其他客户将面临采用障碍。

分布式信任

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

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

便捷性

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

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

设计

政策

我们将把文档中介绍的 GN 最佳实践提升为 一般政策,并将其应用范围扩大到 BUILD.gn 文件之外。 具体而言,我们将建议以下内容:

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

违规处置

我们将引入新工具来清理 Fuchsia 构建,以防止工具调用和工件中出现绝对路径。这些工具将在 CQ 中使用,以防止回归。

清理

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

实现

违规处置工具的运行方式的实现细节不会达到 RFC 的级别。不过,下面提供了一些想法草图,供好奇的读者参考。

清理 Ninja 文件

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

可以使用 strings 处理这些文件,以生成令牌,然后可以过滤这些令牌,以查找看起来像是绝对路径的令牌。例如,可以将此简单扫描器实现为可以针对所有构建变体运行的主机测试。

清理构建引用的文件

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

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

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

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

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

请注意,我们目前仅使用操作跟踪器来封装自定义操作,这些操作是所有构建操作的一个子集。我们这样做是出于与上述相同的性能考虑。从这个角度来看,进一步依赖操作跟踪可能不是一个稳健全面的策略。

使绝对路径失效

一种简单的方法是,在完全使绝对路径失效的同时,使检出中的所有相对路径保持正常运行,即生成 Ninja,然后将检出目录移到其他位置(或只是重命名),然后构建。

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

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

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

对运行时环境的更改

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

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

安全注意事项

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

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

隐私注意事项

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

测试

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

确保不使用绝对路径的另一种方法是,让它们的存在对工程工作流的关键方面不可容忍。例如,如果绝对路径破坏了某个分发的操作,并且该操作是 CQ 的一部分,那么开发者将无法再引入中断。

文档

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

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

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

我们可以制定一项政策,但不强制执行。可能的结果是无法在分布式构建或可重现性方面取得有意义的进展。

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

在先技术和参考资料

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