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 驱动程序一致性测试,我们现在拥有一个可靠且可承载负载的机制,用于为外部树 (OOT) 执行提供版本化的树内 Python Lacewing E2E 测试。

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

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

利益相关方

教员

  • abarth@google.com

Reviewers:

  • 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 体验和工具团队的成员分享了此提案。该提案还在 FEC 会议中进行了讨论,会议支持正式 RFC 批准。

目标

  • 消除了在 OOT 中运行 Lacewing 测试二进制文件时出现的 Python 运行时兼容性问题
  • 将 Lacewing Python 测试打包为单个密封可执行文件
    • 支持 C 扩展库(例如 Fuchsia Controller
    • 支持数据依赖项(例如 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 二进制文件会退出。为了确保孤岛化,我们还将在命令中设置 PYTHONPATH 环境变量,以确保不会使用环境 Python 安装和库。

性能

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

运行时 - 由于捆绑的 Python 应用具有端到端测试性质,因此对提取阶段发生的小启动开销不敏感。经验基础架构数据显示,未优化的 build 的运行时间不到 6 秒,这在这些测试平均运行时间约为 1 分钟的情况下可以忽略不计。

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

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

  • 构建时:构建包含与测试无关的工件(例如运行时、标准库、C 扩展)的“根”Rust ELF 二进制文件,并将其与测试专用 Zip 归档文件(例如 test.pyz)串联起来。然后,“根”ELF 可以通过 ZipArchive 自行解压缩,以访问其 Zip 内容,就像目前提出的方法一样;只不过,Rust 编译过程只会执行一次,而不是针对每个捆绑的 Lacewing 测试执行一次。

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

  • 大小:与上述构建时优化类似,通过将 Rust ELF 二进制文件拆分为与测试无关的“根”和测试专用 ZIP 归档文件,我们可以将较大的“根”与体积明显更小的测试专用 ZIP 归档文件分开分发。这样,我们就可以节省存储空间和网络带宽,因为在每个捆绑的 Lacewing 测试中重复包含“stem”。如需了解详情,请参阅可执行文件大小

工效学设计

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

向后兼容性

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

测试

1) Rust 捆绑过程和 2) 生成的密封测试的稳定性都将在 Fuchsia 基础架构中进行浸泡测试。我们将对测试稳定性与非容器化测试进行基准测试;如果发现测试失败率/失败率没有明显差异,我们将认为基于 Rust 的捆绑方法已可用于生产环境(例如 CQ 和 SDK 分发)。

风险和资源

可执行文件大小

假设每个捆绑的 Lacewing 二进制文件的存储空间占用量约为 40MB,那么当我们将分布式测试数量扩大到合理的估算值(例如,100 项测试占用 4GB)时,这可能会引起问题。为缓解此问题,可以并行探索以下方法,以便使存储空间和带宽费用与捆绑的 Lacewing 测试数量成亚线性扩缩。

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

    • Lacewing 归档文件中最大的组件(Python 运行时、标准库和 Fuchsia 控制器扩展)只会分发一次。
    • 这种方法可缩减的大小会随着我们分发 OOT 的 Python 应用数量而增加。
  2. 独立分发测试。

    • 与上述内容类似,Lacewing 归档文件中最大的组件会在单个 Rust 应用中分发一次。
    • 测试 PYZ(500KB)会单独分发,并作为运行时参数提供给主测试应用。
  3. 减小了 Fuchsia Controller 的大小。

    • 由于 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 年以来一直有活跃版本),UIt 是 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 Controller 会感知 PyInstaller,并在作为 PyInstaller 可执行文件运行时在临时目录中查找 FIDL IR。

Hermeticity

通过检查其 build 元数据 Analysis.toc,我们确认 PyInstaller 的输出是密封的。其中捆绑的所有内容均来自 $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

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

大小

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

还有许多尚未探索的机会可以缩减 PyInstaller Lacewing 测试的大小:

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

运行时管理和维护

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

跨平台可行性

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

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

PyOxidizer

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

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

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

PyOxidizer 遭拒原因

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

在先技术和参考文档