RFC-0256:为 Lacewing 测试捆绑 Python 应用

RFC-0256:针对 Lacewing 测试的 Python 应用捆绑
状态已接受
区域
  • 构建
  • 软件交付
  • 测试
  • 工具链
说明

建议使用编译后的自解压 Rust 主机二进制文件来捆绑 Python Lacewing 测试应用。

Gerrit 更改
作者
审核人
提交日期(年-月-日)2024-05-22
审核日期(年-月-日)2024-08-08

摘要

建议将 Lacewing 测试捆绑为已编译的 Rust 主机二进制文件,以满足 Fuchsia 近期特定于测试的 Python 应用捆绑和分发需求(例如驱动程序一致性、CTF)。

此提案并未排除未来可探索的替代方案

设计初衷

随着 Fuchsia IDK 中最近添加了 RTC 驱动程序一致性测试,我们现在有了一个可正常运行且可承载负载的机制,用于提供版本化的树内 Python Lacewing E2E 测试,以进行树外 (OOT) 执行。

能够将 Lacewing 测试分发给 SDK 使用者,直接支持 Fuchsia 的 2024 年路线图(即驱动程序一致性测试)。此外,需要版本化测试封装和分发的其他测试工作(例如平台预期测试和主机工具兼容性测试)也将受益。

在扩大此测试分发模型的采用范围之前,我们必须确保其可持续性,并提前解决潜在的技术债务。具体而言,目前要求下游 Lacewing 测试使用者提供自己的 Python 运行时以执行测试,但这种做法容易出错。此提案通过完全移除该要求来解决这一缺点。

利益相关方

辅导员

  • abarth@google.com

审核者

  • abarth@google.com
  • chaselatta@google.com
  • hjfreyer@google.com
  • keir@google.com
  • tmandry@google.com
  • tonymd@google.com

已咨询

  • awdavies@google.com
  • crjohns@google.com
  • cpu@google.com
  • jamesr@google.com

共同化

此提案通过 Google 文档与 FEC、Pigweed、Toolchain、SDK Experiences 和 Tools 团队的成员进行了沟通。该提案还在 FEC 会议上进行了讨论,并获得了正式 RFC 批准的支持。

目标

  • 消除在运行 OOT 时 Lacewing 测试二进制文件出现的 Python 运行时兼容性问题
  • 将 Lacewing Python 测试打包为单个封闭可执行文件
    • 支持 C 扩展库(例如 Fuchsia 控制器
    • 支持数据依赖项(例如 FIDL IR 文件 - 目前是 Fuchsia 控制器所必需的)
  • 支持将 Lacewing 测试与其定义位置捆绑在一起
  • 支持 Linux 主机环境

非目标

  • 支持 Windows 主机环境
  • 支持 Mac 主机环境

提案

我们建议针对 Fuchsia 的即时 Python 应用捆绑需求采用基于 Rust 的自定义方法,因为这种方法既可行,又符合上述所有目标

概览

运行 Lacewing 测试所需的所有 Python 组件都将作为数据资源嵌入到编译后的 Rust 二进制文件中(通过 Rust 的 include_bytes! 宏内嵌在二进制文件中)。运行时,Rust 二进制文件会将其 Python 内容提取到临时目录中,构建命令并执行 Python 应用。

与领先的替代方案 PyInstaller 相比,基于 Rust 的自定义方法目前更受青睐,因为它可以灵活地用于 Fuchsia.git(有关详情,请参阅下文的 PyInstaller 拒绝理由部分)。下表总结了这两种方法之间的主要利弊:

  • 基于 Rust 的自定义方法的优势:

    • 可在 Fuchsia.git 中使用
    • 实现,并且在本地环境和基础架构环境中均成功运行
    • 采用 Fuchsia 现有 Rust 主机工具链的密封性保证
    • 无需额外维护动态链接的 Python 运行时
    • 不需要更新 Fuchsia 控制器来查找嵌入式 FIDL-IR 文件
  • PyInstaller 的优势:

    • 内置对所有 Python 应用的通用支持
    • 更好地支持非 Linux 主机平台(例如 Windows)

实现

Lacewing Rust 二进制文件包含 3 个逻辑部分:Lacewing 制品嵌入、制品提取和测试执行。

嵌入

运行 Lacewing 测试所需的所有组件都将作为单个归档文件嵌入到 Rust 二进制文件中。

目录

嵌入式归档文件包含以下内容:

  • 静态链接的 Python 运行时及其标准库(Python 模块)
  • 采用 PYZ 格式的 Lacewing 测试(与 Fuchsia.git 中使用的 zipapp 捆绑包一致)
  • Fuchsia 控制器共享库
  • FIDL IR 文件(目前是 Fuchsia 控制器所必需的)

“Compression”(压缩)

为简单起见,上述所有内容都压缩到一个归档中。当前实现使用 ZIP 格式,该格式会生成大约 40MB 的剥离版 linux-x64 主机二进制文件。未来的迭代可能会选择具有更好压缩比的压缩格式,例如 zstd(在主机驱动的系统测试中,压缩性能并不那么重要)。

构建

为了捆绑任意 Lacewing 测试,我们将添加 GN 构建自动化功能,以动态提供用于嵌入的 Lacewing 制品。

我们通过结合使用 rustc_embed_files()rustc_binary() GN 模板来实现此目的,其中前者以 Rust 库的形式提供特定于测试的数据资源,而后者包含用于提取和执行与测试无关main() 逻辑。

提取和执行

运行时,Rust 二进制文件首先会在临时目录中解压缩其嵌入式资源。然后,它会通过引用提取的内容来构建运行 Lacewing 测试的命令。最后,执行该命令,Rust 二进制文件退出。为确保 hermeticity,我们还将在命令中设置 PYTHONPATH 环境变量,以确保不会使用环境 Python 安装和库。

性能

构建时间 - Lacewing 测试捆绑只会针对 Fuchsia 的一小部分 Lacewing 测试运行,因此 Rust 二进制文件捆绑的构建开销可以忽略不计。

运行时 - 由于捆绑的 Python 应用具有 E2E 测试性质,因此它们对提取阶段发生的微小启动开销并不敏感。实证基础架构数据显示,未优化的 build 的时间小于 6 秒,考虑到这些测试平均需要大约 1 分钟才能运行,因此可以忽略不计。

大小 - 输出的可执行文件大小约为 40MB,与 PyInstaller 等替代方案类似。不过,在嵌入期间使用的压缩算法选择方面仍有改进空间。

下面列出了可用于优化上述每个维度的性能的机会,随着捆绑的 Lacewing 测试数量增加,您可以探索这些机会:

  • 构建时:构建一个包含与测试无关的制品(例如运行时、标准库、C 扩展程序)的“主干”Rust ELF 二进制文件,并将其与特定于测试的 Zip 归档文件(例如 test.pyz)连接起来。然后,通过 ZipArchive 自解压“主干”ELF 即可访问其 Zip 内容,就像当前提议的方法一样;不同之处在于,Rust 编译过程仅执行一次,而不是针对每个捆绑的 Lacewing 测试执行一次。

  • 运行时 - 使用 GN 配置优化 Rust 编译速度。可以测量提取时间并将其导出到性能跟踪后端,以防止出现性能回归。

  • 大小:与上述构建时间优化类似,通过将 Rust ELF 二进制文件拆分为与测试无关的“主干”和特定于测试的 Zip 归档,我们可以将较大的“主干”与明显较小的特定于测试的 Zip 归档分开分发。这样一来,我们就可以节省存储空间和网络带宽,而无需在每个捆绑的 Lacewing 测试中冗余地包含“词干”。如需了解详情,请参阅可执行文件大小

工效学设计

此提案改进了 OOT Lacewing 测试执行的用户体验 - 集成者现在只需要启动单个测试可执行文件。

向后兼容性

添加封闭模式(Rust 捆绑)不会要求对 Lacewing 测试源进行任何更改。换句话说,Lacewing 测试可以在封闭式 (./test) 和非封闭式 (./python test.pyz) 模式下运行,而无需对 Python 源代码进行任何更改。

测试

Rust 捆绑过程和生成的 hermetic 测试的稳定性都将在 Fuchsia 基础架构中进行浸泡测试。我们将针对非封闭式对应项对测试稳定性进行基准比较;如果未发现明显的抖动/失败率差异,我们将认为基于 Rust 的捆绑方法已准备好用于生产(例如 CQ 和 SDK 分发)。

风险与资源

可执行文件大小

假设每个捆绑的 Lacewing 二进制文件的存储空间占用量约为 40MB,那么随着分布式测试数量增加到合理估计值(例如 100 次测试需要 4GB),这可能会引起担忧。为了缓解此问题,可以并行探索以下方法,以使存储和带宽成本随捆绑的 Lacewing 测试数量呈次线性增长。

  1. 在单体式 Rust 应用中分发所有测试。

    • Lacewing 归档的最大组成部分(Python 运行时、标准库和 Fuchsia 控制器扩展程序)仅分发一次。
    • 我们分发的 OOT Python 应用越多,这种方法提供的节省空间效果就越好。
  2. 独立分发测试。

    • 与上述类似,Lacewing 归档的最大组件在单个 Rust 应用中仅分发一次。
    • 测试 PYZ (500kB) 会单独分发,并作为运行时实参提供给主测试应用。
  3. 减小 Fuchsia 控制器的大小。

    • 由于 Fuchsia 控制器向静态 Python 绑定(上游运行时逻辑到 fidlgen)的迁移,这种情况会自然而然地减少。
      • 将移除 fidl_codec.so
      • 静态 Python FIDL 绑定将比 FIDL IR 文件更简洁,因此更小
  4. 探索上述其他压缩格式和参数。

跨平台可行性

将 Python 捆绑解决方案与 Fuchsia 的 Rust 主机工具链相结合意味着,我们将免费获得 Linux x64 支持,但也会受到当前支持的平台集的限制。目前,Linux x64 足以满足我们的迫切需求。不过,如果需要额外的宿主平台支持(例如 Windows),我们需要与 Fuchsia Rust 和 Fuchsia Build 团队合作,以确定可行性范围。

在撰写本文时,OOT Lacewing 测试没有已知的非 Linux-x64 主机平台要求。

有限支持

当前提案仅支持 Lacewing Python 应用。 不过,如果需要,这种方法可以合理地扩展,以支持通用 Python 应用,与探索替代方案(例如 PyInstaller/PyOxidizer)相比,所需的工作量要少得多。

如果需求发生重大变化,例如:替代方案值得考虑:

  • Windows 支持
  • 逐位可重现性
  • 不启动 Python 幕后进程

安全注意事项

在 Fuchsia 的 SDK 中交付 Rust 主机二进制文件是一个成熟的流程(例如 ffx),因此对 Lacewing 测试应用相同的方法不会引入额外的安全风险。

隐私注意事项

不适用

文档

不适用

替代方案

PyInstaller

我们特意添加了有关 PyInstaller 的这一大段内容,以供后人参考。以下调查结果有助于我们今天提出选择自定义的基于 Rust 的方法的建议,并有助于为未来的任何努力提供背景信息。

PyInstaller 拒绝理由

由于 PyInstaller 的许可,它只能在 Fuchsia.git 中作为预构建制品使用,位于 Fuchsia 的开源许可政策的“开发目标”部分下。不过,PyInstaller 旨在作为源代码运行,无法轻松打包到预构建版本中。目前,我们没有动力来解决这个非平凡问题,因为我们已经有了一个基于 Rust 的解决方案,该解决方案可以正常运行,可在源代码树中使用,并且可以满足我们的迫切需求。

概览

PyInstaller 是一个开源第三方 Python 库,用于将 Python 应用及其依赖项捆绑到独立的执行文件中,这些文件可以在未安装 Python 的最终用户系统上运行。由于其易用性和成熟度(自 2015 年以来一直有活跃版本),PyInstaller 是 Python 应用打包领域中较受欢迎的解决方案之一。

当 PyInstaller 在“单文件”模式下运行时,它会将启动时所用的运行时与所有应用模块一起捆绑到输出可执行文件中。当输出可执行文件运行时,PyInstaller 的引导加载程序会将其嵌入式归档文件(包含 Python 解释器、内置/用户提供的 Python 模块、内置/用户提供的共享库和数据文件)提取到临时目录中,然后在密封的环境中执行解释器。

借助 PyInstaller,我们可以将 Lacewing 测试捆绑到单个封闭式可移植可执行文件(Python 运行时 + 测试模块 + 库模块 + Fuchsia 控制器 C 扩展程序 + 数据依赖项,例如 FIDL IR)中,以便在各种 Linux 主机环境中运行。

遗憾的是,静态链接的 Python 运行时(例如 Fuchsia 的供应商 Python)与 PyInstaller 不兼容(相关问题:12)。解决方案是在 Chromium 的 3PP 基础架构(第三方软件包)中构建相同 CPython 运行时的动态链接版本。构建完成后,动态链接的版本(以下简称 DL-CPython3)可以固定为 Fuchsia.git 中的预构建版本,以满足 Fuchsia 的 Python 应用打包需求。

由于用于构建 DL-CPython3 的源代码与 Fuchsia 中已使用的静态链接版本(以下简称 ST-CPython3)相同,因此无需为 DL-CPython3 申请额外的许可批准。

可运行的原型

Fuchsia.git CL 演示了通过 Fuchsia 的 GN 构建系统使用 PyInstaller 打包 Lacewing 测试的可行性。通过原型 CL,我们确认可以在 GN 时通过在源上运行 PyInstaller 来生成密封的 Lacewing Python 测试可执行文件。

由于原型中使用了未提交的依赖项,因此在撰写本文时,无法演示 CL 在 Fuchsia 基础架构中的运行情况。不过,我们预计在 Fuchsia 源代码中提供这些依赖项后,基础架构支持方面不会出现任何问题。

未提交的依赖项
  • DL-CPython3:此功能现已内置在 Chromium 3PP 中,但尚未在 Fuchsia.git 中固定。

  • PyInstaller 库:PyInstaller 及其传递性 Python 依赖项已完成 OSRB 审核,但只能在 Fuchsia.git 中作为预构建的二进制文件使用。

Fuchsia 控制器

需要更新 Fuchsia 控制器,以便在运行时找到其数据依赖项。当 PyInstaller 可执行文件运行时,其内容会被解压缩并从临时目录执行。借助此补丁,Fuchsia 控制器可以识别 PyInstaller,并在作为 PyInstaller 可执行文件运行时在临时目录中查找 FIDL IR。

Hermeticity

通过检查 PyInstaller 的 build 元数据(即 Analysis.toc),确认其输出是密封的。其中捆绑的所有内容均来自 $FUCHSIA_DIR,换句话说,不包含任何环境系统共享库。

此外,在 PyInstaller 输出上运行 ldd 会显示一组共享库依赖项,这些依赖项在现代 Linux 发行版上无处不在且可靠兼容。

~/fuchsia$ ldd dist/soft_reboot_test_fc
linux-vdso.so.1
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2
libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
/lib64/ld-linux-x86-64.so.2

为防止密封性随时间推移而降低,可以添加 build-time 检查,以确认 PyInstaller 可执行文件的 ldd 输出是否存在于许可名单中。

大小

在原型中未考虑任何大小优化的情况下,通过 PyInstaller 打包的软重启 Lacewing 测试大约为 43 MB(供参考,PYZ 版本约为 400KB)。

在缩小 PyInstaller Lacewing 测试规模方面,还有各种未探索的机会:

  1. 运行 PyInstaller 时使用 UPX 压缩。
  2. 排除未使用的内置扩展模块。

运行时管理和维护

Fuchsia 团队需要同时维护 DL-CPython3(用于版本化 Lacewing 测试封装)和 ST-CPython3(用于其他所有内容),并确保这两个版本在功能上等效且同步更新(例如,共享相同的 CIPD 版本标记)。这需要与 Google 的内部 PEEP 软件部署团队合作,以简化 3PP 配置,确保以编程方式防止两个版本出现差异。

跨平台可行性

PyInstaller 不支持交叉编译,因此我们需要在各自的目标环境(例如 Windows、Mac)中运行 PyInstaller,以生成特定于宿主平台的执行文件。

举例来说,为了支持 Windows 最终用户,我们需要在 Windows 机器上使用 Windows 版 DL-CPython3 运行 PyInstaller。在撰写本文时,Fuchsia 还没有基于 Windows 的此类构建环境,因此需要确定范围,以便在我们的构建集群中添加支持。至于 Windows 版 DL-CPython3,Chromium 3PP 已经支持为多个平台(包括 Windows)构建 ST-CPython3,因此如果需要,我们应该能够轻松扩展对 Linux 以外的平台的支持。

PyOxidizer

PyOxidizer 是另一种有吸引力的通用 Python 应用打包选项,与 PyInstaller 相比具有以下各种优势:

  • 密封性 - PyOxidizer 的输出是已编译的二进制文件,而 PyInstaller 的输出是一个需要在运行时解压缩为 Python 位(例如内置 Python 扩展程序)的归档文件。
  • 速度 - PyOxidizer 的输出无需解压缩即可运行,因此其启动时间比 PyInstaller 的输出更快。
  • 安全性 - PyOxidizer 是用 Rust 编写的,与 PyInstaller 的 Python 和 C 相比,Rust 是一种具有更好安全保证的语言。

不过,在 E2E 测试环境中,这些优势大多可以忽略不计。

PyOxidizer 拒绝理由

最终,我们没有选择 PyOxidizer,因为它不符合将 C 扩展库捆绑到单个可执行文件中的目标。使用 PyOxidizer 进行的粗略探索未能将玩具 C 扩展程序捆绑到其输出可执行文件中。虽然鉴于 PyOxidizer 的高度可配置性,实验可能存在一些配置错误,但由于 PyOxidizer 相对较新且采用率较低,因此此类主题的资源很少,我们目前已搁置进一步探索。

在先技术和参考资料