RFC-0119:系统绝对路径被视为有害 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 制定政策,尽可能在所有位置使用相对路径。 |
问题 | |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2021-06-20 |
审核日期(年-月-日) | 2021-07-28 |
摘要
- 在多个枚举实例中制定政策,以便优先使用相对路径或源绝对路径,而不是系统绝对路径,并着眼于未来的用例。
- 改为以自动化方式强制执行此政策,建立回归停止点。
- 清理系统绝对路径的现有用法。
背景
熟悉该主题的读者可以跳过此部分。
路径
定义
读者应熟悉路径的概念。
下面使用以下定义:
- 系统绝对路径:以本地文件系统为根目录的路径。通常用
/
前缀表示。 - 源代码绝对路径或项目绝对路径:相对于源代码树或项目检出的根目录的路径。通常用
//
前缀表示。 - 相对于当前工作目录的路径:相对于当前工作目录的路径。
在下文中,我们将系统绝对路径称为绝对路径,将其他路径称为相对路径,因为它们是相对于另一个路径的相对路径。
Fuchsia 构建系统中的路径
路径在构建系统中用于引用操作的输入和输出文件。Fuchsia 构建系统使用 GN 来定义其构建图。有一个已建立的最佳实践,即最好将 GN 中的路径表示为相对于其他根目录(例如根 build 目录或源根目录)的路径。
生成的代码中的路径
代码引用路径的原因有很多。已检出的源代码无法引用绝对路径,因为它们不可移植 - 它们在提交队列 (CQ) 机器上没有意义,并且会被拒绝;即使它们有意义,当其他工程师检出相同的源代码时,相同的绝对路径在他们的机器上也没有意义。不过,在特定机器上生成且未签入的代码可能包含绝对路径,并且仍然可以正常运行(成功构建和/或运行)。
Fuchsia 会利用许多工具来生成源代码。例如,FIDL 使用 fidlc
,而 Banjo 使用类似的工具。
设计初衷
在构建系统、工具调用、生成的源代码和其他工件中,最好使用相对路径而非绝对路径,原因有很多。
可移植工件
如上所述,相对路径是可移植的,因为它们相对于双方可以达成一致的参考点(例如 Fuchsia 源代码检出的根目录或 build 输出目录的根目录)。在许多情况下,不仅最好使路径可移植,而且这是必须的。
适用于分布式 build 的可移植工件
分发构建操作是指将构建操作的输入(例如将 C/C++ 文件编译为对象文件)发送到远程服务器以进行执行。远程服务器可以代表客户端执行操作并返回结果。通常,远程服务器会依赖于基于内容的缓存来完全跳过该操作。
分发 build 操作有许多好处,本文档未对此进行介绍。
由于客户端和服务器的绝对路径可能不一致,因此在指定要分发的调用的详细信息时,以及在任何上传工件的内容中(它们会相互引用),都可能需要使用相对路径。对于依赖于缓存的分布式构建系统,这一点尤为重要,因为在这种系统中,调用详细信息会用作缓存键的一部分。即使允许使用绝对路径,其使用也可能会破坏缓存机制,因为两个客户端可能会检出相同的源代码,但向服务器发送的请求在绝对路径方面有所不同。
之前,build 路径或生成的代码中存在绝对路径会导致分布式 build 出现问题。例如,Fuchsia 开发者可以使用 Goma 分发 C/C++ 构建操作。在引入通过 C/C++ 包含目录使用绝对路径的更改之前,Goma 的 Fuchsia 用户曾遇到服务中断问题。在某些情况下,分布式 build 会失败并强制执行本地回退,速度较慢。在其他情况下,分布式 build 会成功,但无法命中缓存,导致后端负载增加了数个数量级,进而导致级联故障。
分发 build 操作时出现的另一种类似失败模式是工具调用中的绝对路径。我们之前发现,在分发操作之前检查是否存在此类路径很有用,并以构建操作失败和实用错误的形式拒绝它们。
在其他情况下,绝对路径会导致构建正确性问题。
适用于流水线的便携工件
分发操作时,有时需要为不同的任务提供远程服务器流水线。例如,有些机器可能更适合运行 build,而有些机器可能更适合运行已构建的测试。有时,操作会表示为分叉和联接的图表,例如构建一组测试,然后分叉到多台机器(每台机器运行测试的一个分片),然后联接结果。
在前面的示例中,路径是在客户端和服务器之间交换的,而在本示例中,路径是在流水线中的不同服务器之间交换的。尽管交换的性质不同,但出于同样的原因,相对路径也是首选。
绝对路径可能会导致流水线中断。例如,过去,生成测试覆盖率的流水线会在以下情况下中断:在较早阶段生成的覆盖率报告包含源代码的绝对路径,但在运行覆盖率报告生成流水线后续阶段的不同服务器中无法解析这些路径。生成覆盖率映射文件的工具使用绝对路径,我们更改了这些路径,使其相对于给定基准文件。
使用其他形式的代码插桩时也出现了类似的故障 - 绝对路径曾经在调试信息文件中用于记录,这些记录用于将相对 PC 偏移量解析为源代码行。
用于缓存的便携工件
构建输出可能会被缓存以加快后续构建速度,这称为增量构建。此类缓存通常存储在本地,例如在开发者的工作站上或 build 服务器的特定实例上。从理论上讲,build 缓存也可以在不同的 build 工作器(工作站和服务器)之间交换,前提是网络容量足以支持,并且没有安全和隐私问题。
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 项目不同的 build 规则和工具系统,以便更好地满足不同的需求。例如,其中一些客户的运维规模可能非常大,因此增量构建是必不可少的,缓存也是如此。如果 Fuchsia 在实现这些特性方面存在障碍,那么 Fuchsia 开发者和其他客户在采用 Fuchsia 时也会遇到障碍。
分布式信任
如果 build 的所有工件都是可重现的,那么这为构建系统打开了新特性的大门。例如,可重现的 build 可以作为加密信任链的分布式替代方案,用于验证分布式二进制文件的完整性。不信任的各方只需尝试使用相同的源代码和构建系统来重现这些二进制文件,即可对其进行审核。无法重现二进制文件可能是恶意篡改的证据。
如果工件中使用绝对路径,则不信任的各方将永远无法重现完全相同的结果。
便捷性
在排查问题时,相对路径更易于使用。通常,我们希望结果是一致的,因此可以比较成功操作和失败操作之间的路径,找出任何有意义的差异。
在完全本地工作时,绝对路径非常方便。例如,可以从工具调用中复制绝对路径,并在其他本地 shell 环境中使用,并且绝对路径不对当前工作目录敏感,因此保证能够正常运行。此外,任何两个绝对且经过标准化处理的路径(例如,.
和 ..
部分已解析、已跟踪链接等)都可以通过字符串身份进行等效性检查。由于任何路径都可以设为绝对路径并经过规范化处理,并且此类转换是幂等的,因此可以为本地环境中的路径提供简单的等效性检查。不过,如果用户认为对绝对路径执行此转换更方便,则没有任何限制。不过,由于从相对形式转换为绝对形式和/或标准化形式是破坏性的,因此无法执行相反的转换。因此,优先使用相对形式可以涵盖更多用例。
设计
政策
我们将记录的 GN 最佳实践提升为一般政策,并将其应用于更广泛的范围,而不仅仅是 BUILD.gn
文件。具体而言,我们会建议您采取以下措施:
- 构建系统作为命令行参数传递给工具的路径应相对于调用工具的当前工作目录(对于 GN/Ninja,该目录表示为
root_build_dir
)。 - 构建时生成的文件中的路径应相对于同一根构建目录。例如:生成的源代码、软件包清单、depfiles。
- 在运行时生成的路径应相对于项目源根目录。例如:崩溃中的文件信息、测试覆盖率报告。
违规处置
我们将引入新工具,以便对 Fuchsia build 进行排错,确保工具调用和工件中不存在绝对路径。这些工具将在 CQ 中运行,以防止出现回归问题。
清理
上述工具将提供用于管理许可名单的功能,该功能将在初始化时列出所有现有的违规行为。我们将启动清理工作,将许可名单的大小缩减为零。在正常情况下,回归问题不会被列入许可名单。
实现
违规处置工具的运作方式实现细节不属于 RFC 级别。不过,为了方便好奇的读者,我们在下方提供了一些想法草图。
对 Ninja 文件进行排错
运行 GN 会生成一个 build.ninja
文件,用于描述 build 图。此图的说明包含所有工具调用,包括要调用的工具的路径以及作为参数传递给这些工具的路径。使用的其他文件在 depfile 中引用。
这些文件可以使用 strings
进行处理,以生成令牌,然后过滤这些令牌,使其看起来像绝对路径。例如,此简单扫描器可以实现为主机测试,该测试可以根据所有 build 变体运行。
对 build 引用的文件进行排错
除了对 Ninja 文件进行脱敏处理外,我们还可以对指定为 build 操作的输入或输出的任何文件进行标记化和脱敏处理。假设 build 是密封的,我们将能够从 Ninja 图或 depfile 中发现所有此类文件。
检查是否存在绝对路径字符串
我们可以扫描 build 输出目录 (out/
) 下的所有文件,生成字符串,并检查是否有任何绝对路径,然后发出错误。这并非万无一失的保护措施,而是一道简单的额外防线。
在操作跟踪器中拒绝绝对路径
我们已经有了用于封装 GN 操作的工具,并且已经在使用该工具拒绝 depfile 中的绝对路径。我们可以进一步扩展此机制。
请注意,我们目前仅使用操作跟踪器封装自定义操作,而自定义操作只是所有构建操作的一部分。我们之所以这样做,是因为存在上述性能问题。从这个角度来看,仅仅依赖于操作跟踪可能并不是一项可靠的全面策略。
使绝对路径失效
如需让检出内容中的所有相对路径保持有效状态,同时完全使绝对路径失效,一种简单的方法是生成 Ninja,然后将检出目录移至其他位置(或直接重命名),然后进行构建。
$ fx gen
$ mv $FUCHSIA_DIR ${FUCHSIA_DIR}_renamed
$ fx build
如果任何 build 调用都将检出的路径引用为绝对路径,则 build 将失败。
这种方法非常易于实现、可移植且没有性能开销。一些缺点包括,在发生故障时,错误消息会让新手感到困惑,并且生成的工件(例如 depfile、srcgen、调试信息)中仍然可能会泄露绝对路径。
运行时环境的变更
另一种方法是更改 build 的运行时环境,使绝对路径无效或无害。例如,某些项目使用 chroot
等概念在结账根目录中构建沙盒。其他构建系统使用特殊文件系统来实现沙盒化。
运行时方法值得考虑,因为它们可以提供更强的正确性保证。不过,需要考虑性能问题和棘手的可移植性 问题。
安全注意事项
路径可能会在以下位置使用,因为路径的解释(它解析为哪个实际文件)可能会影响敏感的系统行为。例如,可作为可执行页面映射到内存的文件的许可名单。
在这种情况下,最好使用相对于项目的路径,而不是相对于包含指定列表的目录的路径,也不是相对于处理此列表的工具的 CWD 的路径。这是因为,相对于项目的路径在其定义的项目中具有明确的解析,而其他形式的相对路径可能会根据全局可变状态(例如 CWD)进行不同的解读。
隐私注意事项
绝对路径有时可能会泄露个人身份信息。例如,用户的用户名通常是包含该用户的检出或构建输出目录中的文件的绝对路径的一部分。将绝对路径替换为相对于来源的路径可消除此个人身份信息泄露途径。
测试
上面,我们探讨了一些用于实现可捕获绝对路径使用情况的检查方法。由于这些检查是作为构建步骤或主机测试实现的,因此它们可以在 CQ 中运行,并充当持续测试。
确保不使用绝对路径的另一种方法是,让工程工作流的关键方面无法容忍绝对路径的存在。例如,如果绝对路径破坏了分发的某个操作,而该操作是 CQ 的一部分,那么开发者将无法再引入破坏。
文档
当强制要求路径不为绝对路径的工具失败时,它们应生成指向相应问题排查页面的错误。举个例子,当强制要求 build 操作是密封的操作跟踪器失败时,它会生成一条错误消息,其中包含指向密封 build 操作页面的链接。
缺点、替代方案和未知情况
我们无能为力,错失了分布式构建空间和可重现性空间中的机会。
我们可以制定政策,但无法强制执行。可能的后果是,您将无法在分布式 build 或可重现性方面取得实质性进展。
我们可以暂时忽略这个问题,但代价是随着时间的推移需要进行更多回归,这正是熵衰减的特性。
在先技术和参考文档
一位伟大的绝地大师曾说过:“只有西斯才会绝对化。”