PEP 730 – 将 iOS 添加为支持平台
- 作者:
- Russell Keith-Magee <russell at keith-magee.com>
- 发起人:
- Ned Deily <nad at python.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2023年10月9日
- Python 版本:
- 3.13
- 决议:
- Discourse 消息
摘要
此 PEP 提议将 iOS 添加为 CPython 中的支持平台。最初的目标是为 Python 3.13 实现 Tier 3 支持。此 PEP 描述了支持 iOS 所需更改的技术方面。它还描述了与将 iOS 采用为 Tier 3 平台相关的项目管理问题。
动机
在过去的 15 年中,移动平台已成为计算领域中越来越重要的组成部分。iOS 是控制绝大多数此类设备的两个操作系统之一。然而,CPython 中没有对 iOS 的官方支持。
BeeWare 项目和 Kivy 都已支持 iOS 近 10 年。此支持能够生成已获准在 iOS 应用商店发布的应用。这证明了 iOS 支持的技术可行性。
对于 Python 作为一种语言的未来而言,它能够用于任何广泛采用的硬件或操作系统至关重要。如果 Python 不能用于广泛使用的平台,潜在用户将采用其他提供这些平台支持的语言,从而影响该语言的采用。
基本原理
开发环境
iOS 提供单个 API,但有 2 个不同的 ABI - iphoneos(物理设备)和 iphonesimulator。每个 ABI 都可以在多个 CPU 架构上提供。撰写本文时,Apple 官方支持设备 ABI 上的 arm64,以及模拟器 ABI 上支持 arm64 和 x86_64。
与 macOS 一样,iOS 支持创建包含多个 CPU 架构的“胖”二进制文件。但是,胖二进制文件 不能 跨越 ABI。也就是说,可以有一个胖 模拟器 二进制文件和一个胖 设备 二进制文件,但不可能创建单个涵盖模拟器和设备需求的胖“iOS”二进制文件。为了支持单个开发工件的分发,Apple 使用“XCframework”结构——一个围绕多个 ABI 的包装器,它们实现了一个通用 API。
iOS 运行在 Darwin 内核上,类似于 macOS。然而,在实现层面需要区分 macOS 和 iOS,因为 iOS 和 macOS 之间存在显着的平台差异。
iOS 代码被编译以与最低 iOS 版本兼容。
Apple 经常在他们的营销材料中提及“iPadOS”。然而,从开发角度来看,iPadOS 和 iOS 之间没有明显的区别。为 iphoneos 或 iphonesimulator ABI 编译的二进制文件可以部署在 iPad 上。
其他 Apple 平台,如 tvOS、watchOS 和 visionOS,使用不同的 ABI,不属于此 PEP 的范围。
POSIX 合规性
iOS 大体上是一个 POSIX 平台。然而,类似于 WASI/Emscripten,iOS 上存在一些 POSIX API,但无法使用;还有一些 POSIX API 根本不存在。
其中最值得注意的是 iOS 不提供任何形式的多进程支持。fork 和 spawn 在 iOS API 中都 存在;但是,如果它们被调用,调用 iOS 进程会停止,并且新进程不会启动。
与 WASI/Emscripten 不同,iOS 支持 线程。
套接字处理也存在显著限制。由于进程沙盒,无法通过套接字进行进程间通信。但是,网络通信的套接字 可用。
动态库
iOS App Store 指南允许应用使用 Objective C 或 Swift 以外的语言编写。但是,他们对提交分发的应用结构有非常严格的规定。
iOS 应用可以使用动态加载的库;但是,对于如何在 iOS 上打包动态加载的内容以供使用,有非常严格的要求。
- 动态二进制内容必须编译为动态库,而不是共享对象或二进制包。
- 它们必须作为 Frameworks 打包在应用程序包中。
- 每个 Framework 只能包含一个动态库。
- Framework 必须 包含在 iOS App 的
Frameworks文件夹中。 - Framework 不得包含任何非库内容。
这给 CPython 的操作带来了一些限制。不可能将二进制模块存储在 lib-dynload 和/或 site-packages 文件夹中;它们必须存储在应用程序的 Frameworks 文件夹中,每个模块都包装在一个 Framework 中。这也意味着 Python 模块可以通过使用 Python 模块的 __file__ 属性来构建二进制模块位置的常见假设不再成立。
与 macOS 一样,编译可从 Python 静态链接构建访问的二进制模块需要使用 --undefined dynamic_lookup 选项,以避免将 libpython3.x 链接到每个二进制模块中。然而,在 iOS 上,使用此编译器标志会引发弃用警告。在 macOS 上也观察到此标志的警告——然而,Apple 员工的回复表明他们 不打算通过删除此选项来破坏 CPython 生态系统。由于 Python 目前在 iOS 上没有显着的存在,因此很难判断 iOS 对此标志的使用是否属于同一范畴。
控制台和交互式使用
分发传统的 CPython REPL 或交互式“python.exe”不应被视为此工作的目标。
移动设备(包括 iOS)不提供 TTY 风格的控制台。它们不提供 stdin、stdout 或 stderr。iOS 提供一个系统日志,并且可以安装重定向,以便所有 stdout 和 stderr 内容都重定向到系统日志;但没有 stdin 的模拟。
此外,iOS 限制在运行时下载额外的代码(因为此行为在功能上与试图规避 App Store 审查没有区别)。因此,传统的“创建虚拟环境并 pip install”开发体验在 iOS 上将不可行。
可以 构建提供 REPL 接口的本地 iOS 应用程序。这将更接近 IDLE 风格的用户体验;但是,Tkinter 不能在 iOS 上使用,因此任何应用程序都需要从头重写。iOS 应用商店中已经包含了几款此类应用程序的示例(例如,Pythonista 和 Pyto)。这项工作的重点是提供一个嵌入式分发,供 IDE 风格的本地接口使用,而不是面向用户的 iOS Python“应用程序”接口。
规范
平台识别
sys
sys.platform 在模拟器和物理设备上都将标识为 "ios"。
sys.implementation._multiarch 将描述 ABI 和 CPU 架构
"arm64-iphoneos"适用于 ARM64 设备"arm64-iphonesimulator"适用于 ARM64 模拟器"x86_64-iphonesimulator"适用于 x86_64 模拟器
platform
platform 将被修改以支持返回 iOS 特定细节。除了以下情况,platform 模块返回的大多数值将与 os.uname() 返回的值匹配
platform.system()-"iOS"或iPadOS(取决于使用的硬件),而不是"Darwin"platform.release()- iOS 版本号,以字符串形式(例如,"16.6.1"),而不是 Darwin 内核版本。
此外,将添加一个 platform.ios_ver() 方法。这与 platform.mac_ver() 类似,后者可用于提供 macOS 版本信息。ios_ver() 将返回一个命名元组,其中包含以下内容
system- 操作系统名称(iOS或iPadOS,取决于硬件)release- iOS 版本,以字符串形式(例如,"16.6.1")。model- 设备的型号标识符,以字符串形式(例如,"iPhone13,2")。在模拟器上,这将返回"iPhone"或"iPad",具体取决于模拟器设备。is_simulator- 一个布尔值,指示设备是否为模拟器。
os
os.uname() 将返回 POSIX uname() 调用的原始结果。这将导致以下值
sysname-"Darwin"release- Darwin 内核版本(例如,"22.6.0")
此方法将 os 模块视为系统 API 的“原始”接口,并将 platform 视为提供更通用有用值的高级 API。
sysconfig
sysconfig 模块将使用最低 iOS 版本作为 sysconfig.get_platform() 的一部分(例如,"ios-12.0-arm64-iphoneos")。sysconfigdata_name 和 Config makefile 将遵循与现有平台相同的模式(使用 sys.platform、sys.implementation._multiarch 等)来构建标识符。
子进程支持
iOS 将利用 WASI/Emscripten 建立的禁用子进程模式。subprocess 模块将在尝试启动子进程时引发异常,并且 os.fork 和 os.spawn 调用将引发 OSError。
动态模块加载
为了适应 iOS 动态加载,importlib 引导程序将扩展以添加一个元路径查找器,该查找器可以将对 Python 二进制模块的请求转换为 Framework 位置。此查找器仅在 sys.platform == "ios" 时安装。
此查找器将通过使用完整模块名称作为框架名称(即 foo.bar._whiz.framework)将 Python 模块名称(例如 foo.bar._whiz)转换为唯一的框架名称。框架是一个目录;查找器将在该目录中查找名为 foo.bar._whiz 的二进制文件。
编译
唯一受支持的二进制格式是动态可链接的 libpython3.x.dylib,以 iOS 兼容的框架格式打包。尽管 --undefined dynamic_lookup 编译器选项目前有效,但该选项的长期可行性无法保证。为了避免依赖一个未来不确定的编译器标志,iOS 上的二进制模块将与 libpython3.x.dylib 链接。这意味着 iOS 二进制模块将无法由静态链接到 libpython3.x.a 的可执行文件加载。因此,不支持静态 libpython3.x.a iOS 库。这与 CPython 在 Windows 上使用的模式相同。
为 iOS 构建 CPython 需要使用 CPython configure 构建系统中的跨平台工具。一次 configure/make/make install 过程将生成一个 Python.framework 工件,该工件可用于单个 ABI 和架构。
需要额外的工具将多个架构的 Python.framework 构建合并到单个“胖”库中。还需要工具将多个 ABI 合并到 Apple 用于以单个包分发不同 ABI 的多个框架的 XCframework 格式中。
将提供一个 Xcode 项目,用于运行 CPython 测试套件。将提供工具来自动化编译测试套件二进制文件、启动模拟器、安装测试套件和执行测试套件的过程。
分发
将 iOS 添加为 Tier 3 平台仅需要支持从未修补的 CPython 代码检出编译 iOS 兼容构建。它不要求为最终用户生产官方分发的 iOS 工件。
如果/当 iOS 升级到 Tier 2 或 Tier 1 支持时,用于生成 XCframework 包的工具可以用于生产 iOS 分发工件。然后可以将其作为“嵌入式分发”分发,类似于 Windows 嵌入式分发,或者作为可以添加到 Xcode 项目的 CocoaPod 或 Swift 包。
CI 资源
Anaconda 已提出提供物理硬件来运行 iOS 构建机器人。
GitHub Actions 能够在 macOS 机器上托管 iOS 模拟器,并且 iOS 模拟器可以通过脚本环境控制。免费层目前只提供 x86_64 macOS 机器;然而,ARM64 运行器最近在付费计划中提供。但是,为了避免耗尽 macOS 运行器资源,iOS 的 GitHub Actions 运行将不会作为标准 CI 配置的一部分添加。
打包
iOS 将不提供“通用” wheel 格式。相反,将为每个 ABI-arch 组合提供 wheel。
iOS wheel 将使用标签
ios_12_0_arm64_iphoneosios_12_0_arm64_iphonesimulatorios_12_0_x86_64_iphonesimulator
在这些标签中,“12.0”是最低支持的 iOS 版本。与 macOS 一样,该标签将包含编译 wheel 时选择的最低 iOS 版本;使用最低 iOS 版本 15.0 编译的 wheel 将使用 ios_15_0_* 标签。撰写本文时,iOS 12.0 暴露了大多数重要的 iOS 功能,同时覆盖了近 100% 的设备;这将用作 iOS 版本匹配的下限。
这些 wheel 可以就地包含二进制模块(即,与 Python 源代码位于同一位置,与桌面平台的 wheel 相同);但是,它们需要进行后处理,因为二进制模块需要移动到“Frameworks”位置进行分发。这可以通过 Xcode 构建步骤自动化。
PEP 11 更新
PEP 11 将更新以包含两个 iOS ABI
arm64-apple-iosarm64-apple-ios-simulator
Ned Deily 将担任这些 ABI 的初始核心团队联系人。
x86_64-apple-ios-simulator 目标将尽力支持,但不会作为 Tier 3 支持的目标。这是由于 x86_64 作为模拟平台即将弃用,以及目前难以调试 x86_64 macOS 硬件。
向后兼容性
添加新平台不会给 CPython 本身带来任何向后兼容性问题。
如果任何 CPython 补丁的最终形式与历史上使用的补丁不一致,则历史上提供 CPython 支持的项目(即 BeeWare 和 Kivy)可能会有一些向后兼容性影响。
虽然不严格是向后兼容性问题,但确实存在平台采用问题。尽管 CPython 本身可能支持 iOS,但如果如何生成 iOS 兼容的轮子不明确,并且像 cryptography、Pillow 和 NumPy 这样的著名库不提供 iOS 轮子,社区在 iOS 上采用 Python 的能力将受到限制。因此,有必要清楚地记录项目如何将 iOS 构建添加到其 CI 和发布工具中。将 iOS 支持添加到 crossenv 和 cibuildwheel 等工具可能是一种实现方式。
安全隐患
将 iOS 添加为新平台不会增加任何安全隐患。
如何教授此内容
与此 PEP 相关的教育需求主要与最终用户如何将 iOS 支持添加到他们自己的 Xcode 项目有关。这可以通过有关该过程的文档和教程来完成。如果/当支持从 Tier 3 提高到 Tier 2 或 Tier 1 时,对这些文档的需求将增加;但是,这种过渡也应该伴随着简化的部署工件(例如 Cocoapod 或 Swift 包)的推出,这些工件将与 Xcode 开发集成。
参考实现
BeeWare Python-Apple-support 存储库包含一个参考补丁和构建工具,用于编译可分发工件。
Briefcase 提供了一个在 iOS 模拟器上执行测试套件的代码参考实现。Toga Testbed 是一个使用 GitHub Actions 在 iOS 模拟器上执行的测试套件示例。
被拒绝的想法
模拟器识别
此 PEP 的早期版本建议包含 sys.implementation._simulator 属性,以识别代码何时在设备上或模拟器上运行。这因将受保护的名称用于公共 API,以及 sys 命名空间被 iOS 特定细节污染而被拒绝。
讨论期间的另一个提议是包含一个通用的 platform.is_emulator() API,任何平台都可以实现该 API,例如区分在 ARM64 硬件上运行 x86_64 代码,或在 QEMU 或其他虚拟化方法中运行。这被拒绝,理由是尚不清楚“模拟器”的一致解释是什么,也无法在 iOS 之外检测模拟器。
决定将此细节保留为 iOS 特有,并将其包含在 platform.ios_ver() API 中。
GNU 编译器三元组
autoconf 需要使用 GNU 编译器三元组来识别构建和主机平台。然而,autoconf 工具链不提供对 iOS 模拟器的原生支持,因此我们面临着如何将 iOS 硬件挤进 GNU 命名体系的任务。
这可以通过(对 config.sub 进行一些修补)来完成,但这会导致 2 个主要的命名不一致来源
arm64与aarch64作为 64 位 ARM 硬件的标识符;以及- 用于表示模拟器的标识符是什么。
Apple 自己的工具使用 arm64 作为架构,但在某些情况下似乎可以容忍 aarch64。设备平台被标识为 iphoneos 和 iphonesimulator。
Rust 工具链使用 aarch64 作为架构,并使用 aarch64-apple-ios 和 aarch64-apple-ios-sim 来标识设备平台;但是,它们使用 x86_64-apple-ios 来表示 x86_64 硬件上的 iOS 模拟器。
决定使用 arm64-apple-ios 和 arm64-apple-ios-simulator 的原因是
autoconf工具链在config.sub中已经包含对ios作为平台的支持;只有模拟器没有表示。- 主机三元组的第三部分用作
sys.platform。 - 当苹果自己的工具引用 CPU 架构时,它们使用
arm64,并且 GNU 工具链对架构的使用在构建过程之外是不可见的。 - 当苹果自己的工具引用独立于操作系统的模拟器状态时(例如,在 Swift 子模块的命名中),它们使用
-simulator后缀。 - 虽然 一些 iOS 包将使用 Rust,但 所有 iOS 包都将使用 Apple 的工具。
本文档最初接受的版本使用 aarch64 形式作为 PEP 11 标识符;这在最终确定期间进行了修正。
“通用” wheel 格式
macOS 目前支持两种 CPU 架构。为了方便最终用户开发体验,Python 定义了一种“universal2” wheel 格式,其中包含 x86_64 和 ARM64 二进制文件。
在概念上,可以提供类似的“通用” iOS wheel 格式。然而,本 PEP 不采用这种方法,原因有二。
首先,macOS 上的经验,尤其是在数值 Python 生态系统中,是通用 wheel 极难适应。虽然原生 macOS 库保持强大的多平台支持,并且 Python 本身也已更新,但绝大多数上游非 Python 库不提供多架构构建支持。因此,编译通用 wheel 不可避免地需要多次编译,并且对如何分发不同架构的头文件做出复杂的决策。由于这种复杂性,许多流行的项目(包括 NumPy 和 Pillow)根本不提供通用 wheel,而是提供单独的 ARM64 和 x86_64 wheel。
其次,历史经验表明 iOS 需要一个更具流动性的“通用”定义。在过去 10 年中,至少有 5 种不同的“通用”解释适用于 iOS,包括 armv6、armv7、armv7s、arm64、x86 和 x86_64 架构的各种组合,以及设备和模拟器。如果现在定义,“通用-iOS”可能包括模拟器上的 x86_64 和 arm64,以及设备上的 arm64;然而,x86_64 硬件即将弃用将增加另一种解释;将来可能还需要添加 arm64e 作为新的设备架构。将 iOS wheel 指定为仅限单平台意味着 Python 核心团队可以避免就更新的“通用”格式进行持续的标准化讨论。
这也意味着 wheel 发布者可以针对每个项目决定支持哪些平台。例如,一个项目可能会选择放弃 x86_64 支持,或者比 Python 生态系统的其他部分更早地采用新架构。使用平台特定的 wheel 意味着可以将此决定留给各个包发布者。
这个决定以使部署更复杂为代价。然而,iOS 上的部署本身就是一个复杂的过程,最好借助工具。目前,不需要二进制合并,因为只有一种设备上架构,并且模拟器二进制文件不被视为可分发工件,因此只需要一种架构即可为模拟器构建应用程序。
支持静态构建
虽然 --undefined dynamic_lookup 选项的长期可行性无法保证,但该选项确实存在并且有效。一个选择是忽略弃用警告,并希望 Apple 要么撤销弃用决定,要么永不最终确定弃用。
鉴于苹果的决策过程完全不透明,这充其量是一个有风险的选择。再加上更广泛的 iOS 开发生态系统鼓励使用框架,没有静态库的遗留用途需要考虑,并且静态链接的 iOS libpython3.x.a 的唯一好处是应用程序启动时间略微缩短,因此省略对 libpython3.x 静态构建的支持似乎是一个合理的折衷方案。
值得注意的是,关于 macOS 上链接的另一种方法 已经有一些讨论,这将消除对 --undefined dynamic_lookup 选项的需求,尽管关于这种方法的讨论似乎由于实现复杂性而陷入停滞。如果这些复杂性得以克服,很可能相同的方法 也 可以用于 iOS,这 将 使静态链接的 libpython3.x.a 成为可能。
将二进制模块链接到 libpython3.x.dylib 的决定将使将来引入静态 libpython3.x.a 构建变得复杂,因为转换为不同的二进制模块链接方法将需要一种清晰的方法来区分“动态链接”的 iOS 二进制模块和“静态兼容”的 iOS 二进制模块。然而,鉴于静态 libpython3.x.a 缺乏实际好处,似乎不太可能需要进行此更改。
交互式/REPL 模式
传统的 python.exe 命令行体验在移动设备上不太可行,因为移动设备没有命令行。iOS 应用程序没有 stdout、stderr 或 stdin;虽然可以将 stdout 和 stderr 重定向到系统日志,但没有 stdin 的来源,除非构建一个非常特定的面向用户的应用程序,而这更接近于 IDLE 风格的 IDE 体验。因此,决定只关注“嵌入式模式”作为移动分发的目标。
x86_64 模拟器支持
苹果不再销售 x86_64 硬件。因此,调试 x86_64 构建机器人可能很困难。可以在 ARM64 硬件上以 x86_64 兼容模式运行 macOS 二进制文件;然而,这不适合测试目的。因此,x86_64 模拟器 (x86_64-apple-ios-simulator) 不会作为 Tier 3 目标添加。iOS 支持很可能在 x86_64 上无需任何修改即可运行;这只影响 官方 Tier 3 状态。
设备上测试
在模拟器上进行 CI 测试相对容易。设备上测试则困难得多,因为可以配置为提供构建机器人或 Github Actions 运行器的设备农场的可用性有限。
然而,设备测试可能没有必要。作为参考点——Apple 的 Xcode Cloud 解决方案不提供设备上测试。他们依赖于设备和模拟器之间的 API 一致性,并且 ARM64 模拟器测试足以揭示 CPU 特定问题。
platform.ios_ver() 返回的值
本文档最初接受的版本不包含 system 标识符。这在实现阶段添加,以支持 platform.system() 的实现。
本文档最初接受的版本还描述了 min_release 将在 ios_ver() 结果中返回。最终版本省略了 min_release 值,因为它在运行时不重要;它只影响二进制兼容性。最低版本 包含 在 sysconfig.get_platform() 返回的值中,因为这用于定义 wheel(和其他二进制文件)兼容性。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0730.rst