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 提案是在 CPython 中添加 iOS 作为支持平台。初始目标是在 Python 3.13 中实现 3 级支持。本 PEP 描述了支持 iOS 所需更改的技术方面。它还描述了将 iOS 作为 3 级平台进行采用的项目管理问题。
动机
在过去的 15 年里,移动平台已成为计算领域越来越重要的组成部分。iOS 是控制绝大多数此类设备的两个操作系统之一。但是,CPython 没有官方的 iOS 支持。
BeeWare 项目 和 Kivy 都已经支持 iOS 近 10 年了。此支持能够生成已被 iOS App Store 接受发布的应用程序。这证明了 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”结构 - 一个围绕多个实现通用 API 的 ABI 的包装器。
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 上打包动态加载的内容有非常严格的要求
- 动态二进制内容必须编译为动态库,而不是共享对象或二进制包。
- 它们必须作为框架打包在应用程序包中。
- 每个框架只能包含一个动态库。
- 框架 *必须* 包含在 iOS 应用程序的
Frameworks
文件夹中。 - 框架可能不包含任何非库内容。
这给 CPython 的操作带来了一些限制。无法将二进制模块存储在 lib-dynload
和/或 site-packages
文件夹中;它们必须存储在应用程序的 Frameworks 文件夹中,每个模块都包装在一个框架中。这也意味着 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 安装”开发体验在 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"
时才会安装此查找器。
此查找器将 Python 模块名称(例如,foo.bar._whiz
)转换为唯一的 Framework 名称,方法是使用完整模块名称作为框架名称(即,foo.bar._whiz.framework
)。框架是一个目录;查找器将在该目录中查找名为 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 添加为第 3 层平台只需要添加对从未修补的 CPython 代码检出中编译与 iOS 兼容的构建的支持。它不需要为最终用户生成官方分发的 iOS 工件。
如果/当 iOS 更新到第 2 层或第 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 不会提供“通用”轮子格式。相反,将为每个 ABI-arch 组合提供轮子。
iOS 轮子将使用标签
ios_12_0_arm64_iphoneos
ios_12_0_arm64_iphonesimulator
ios_12_0_x86_64_iphonesimulator
在这些标签中,“12.0”是支持的最低 iOS 版本。与 macOS 一样,标签将包含编译轮子时选择的最低 iOS 版本;使用 15.0 的最低 iOS 版本编译的轮子将使用 ios_15_0_*
标签。在撰写本文时,iOS 12.0 公开了大多数重要的 iOS 功能,同时覆盖了近 100% 的设备;这将用作 iOS 版本匹配的下限。
这些轮子可以包含就地二进制模块(即,与 Python 源代码位于同一位置,与桌面平台的轮子相同);但是,需要对其进行后处理,因为二进制模块需要移动到“Frameworks”位置才能进行分发。这可以通过 Xcode 构建步骤自动化。
PEP 11 更新
PEP 11 将更新为包含两个 iOS ABI
arm64-apple-ios
arm64-apple-ios-simulator
Ned Deily 将担任这些 ABI 的初始核心团队联系人。
x86_64-apple-ios-simulator
目标将尽力提供支持,但不会将其作为第 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 项目中相关。这可以通过有关该过程的文档和教程来实现。如果/当支持从第 3 层提升到第 2 层或第 1 层时,对这些文档的需求将会增加;但是,此过渡也应伴随着简化的部署工件(例如 Cocoapod 或 Swift 包),这些工件与 Xcode 开发集成。
参考实现
BeeWare Python-Apple-support 存储库包含一个参考补丁和构建工具,用于编译可分发的工件。
Briefcase 提供了执行 iOS 模拟器上测试套件的代码的参考实现。Toga Testbed 是一个使用 GitHub Actions 在 iOS 模拟器上执行的测试套件示例。
被拒绝的想法
模拟器识别
本 PEP 的早期版本建议包含 sys.implementation._simulator
属性以识别代码是在设备上运行还是在模拟器上运行。由于使用受保护的名称作为公共 API,以及使用 iOS 特定细节污染了 sys
命名空间,因此此建议被拒绝。
在讨论期间,另一个提议是包含一个通用的 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
。 - 当 Apple 自身的工具引用 CPU 架构时,它们使用
arm64
,并且 GNU 工具对架构的使用在构建过程之外不可见。 - 当 Apple 自身的工具引用与操作系统无关的模拟器状态(例如,在 Swift 子模块的命名中)时,它们使用
-simulator
后缀。 - 虽然 *某些* iOS 包将使用 Rust,但 *所有* iOS 包都将使用 Apple 的工具。
本文档最初接受的版本使用 aarch64
形式作为 PEP 11 标识符;这在最终确定过程中得到了纠正。
“通用” wheel 格式
macOS 目前支持 2 种 CPU 架构。为了帮助最终用户开发体验,Python 定义了一种“universal2”轮子格式,其中包含 x86_64 和 ARM64 二进制文件。
从概念上讲,可以提供类似的“通用”iOS 轮子格式。但是,本 PEP 未采用此方法,原因有两个。
首先,macOS 上的体验,尤其是在数值 Python 生态系统中,是通用轮子可能非常难以适应。虽然原生 macOS 库保持强大的多平台支持,并且 Python 本身已更新,但绝大多数上游非 Python 库不提供多架构构建支持。因此,编译通用轮子不可避免地需要多次编译传递,以及关于如何为不同架构分发头文件的复杂决策。由于这种复杂性,许多流行的项目(包括 NumPy 和 Pillow)根本不提供通用轮子,而是提供单独的 ARM64 和 x86_64 轮子。
其次,历史经验表明 iOS 将需要更流畅的“通用”定义。在过去的 10 年中,至少有 5 种不同的“通用”解释适用于 iOS,包括 armv6、armv7、armv7s、arm64、x86 和 x86_64 架构的各种组合,在设备和模拟器上。如果现在定义,“通用-iOS”可能会包含模拟器上的 x86_64 和 arm64,以及设备上的 arm64;但是,x86_64 硬件即将弃用将增加另一种解释;并且将来可能需要添加 arm64e 作为新的设备架构。将 iOS 轮子指定为仅限单平台意味着 Python 核心团队可以避免关于更新的“通用”格式的持续标准化讨论。
这也意味着轮子发布者能够针对哪些平台可行地支持做出每个项目的决策。例如,一个项目可以选择放弃 x86_64 支持,或比 Python 生态系统的其他部分更早地采用新架构。使用特定于平台的轮子意味着此决策可以留给各个包发布者。
此决定是以部署变得更加复杂为代价的。但是,iOS 上的部署已经是一个复杂的过程,最好由工具来辅助。目前,不需要二进制合并,因为只有一个设备上架构,并且模拟器二进制文件不被视为可分发的工件,因此只需要一个架构即可为模拟器构建应用程序。
支持静态构建
虽然 --undefined dynamic_lookup
选项的长期可行性无法保证,但该选项确实存在,并且有效。一个选项是忽略弃用警告,并希望 Apple 撤消弃用决定,或者永远不会最终确定弃用。
鉴于 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 模拟器支持
Apple 不再销售 x86_64 硬件。因此,委托 x86_64 构建机器人可能很困难。可以在 ARM64 硬件上以 x86_64 兼容模式运行 macOS 二进制文件;但是,这对于测试目的来说并不理想。因此,x86_64 模拟器 (x86_64-apple-ios-simulator
) 将不会作为第 3 层目标添加。iOS 支持很可能在 x86_64 上没有任何修改就可以工作;这只会影响*官方*第 3 层状态。
设备上测试
在模拟器上进行 CI 测试可以很容易地适应。设备上的测试要困难得多,因为可以配置为提供 Buildbots 或 Github Actions 运行程序的设备场的可用性有限。
但是,设备上的测试可能不是必需的。作为一个数据点 - Apple 的 Xcode Cloud 解决方案不提供设备上的测试。它们依赖于 API 在设备和模拟器之间一致的事实,并且 ARM64 模拟器测试足以揭示特定于 CPU 的问题。
platform.ios_ver()
返回的值
本文档最初接受的版本没有包含 system
标识符。这在实施阶段被添加以支持 platform.system()
的实施。
本文档最初接受的版本还描述了 min_release
将在 ios_ver()
结果中返回。最终版本省略了 min_release
值,因为它在运行时并不重要;它只影响二进制兼容性。最小版本*包含*在 sysconfig.get_platform()
返回的值中,因为这用于定义轮子(和其他二进制文件)的兼容性。
版权
本文档置于公共领域或根据 CC0-1.0-Universal 许可证,以两者中较宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0730.rst