7. 在 iOS 上使用 Python

作者:

Russell Keith-Magee (2024-03)

在 iOS 上使用 Python 与在桌面平台上不同。在桌面平台上,Python 通常作为系统资源安装,该计算机的任何用户都可以使用。然后,用户通过运行 python 可执行文件并在交互式提示符下输入命令,或通过运行 Python 脚本来与 Python 交互。

在 iOS 上,没有作为系统资源安装的概念。唯一的软件分发单位是“应用”。也没有可以运行 python 可执行文件或与 Python REPL 交互的控制台。

因此,在 iOS 上使用 Python 的唯一方法是嵌入模式——也就是说,通过编写一个原生的 iOS 应用程序,并使用 libPython 嵌入 Python 解释器,然后使用 Python 嵌入 API 调用 Python 代码。完整的 Python 解释器、标准库以及你所有的 Python 代码都会被打包成一个独立的捆绑包,可以通过 iOS App Store 分发。

如果你想首次尝试用 Python 编写 iOS 应用,像 BeeWareKivy 这样的项目将提供更加平易近人的用户体验。这些项目管理着运行 iOS 项目相关的复杂性,所以你只需要处理 Python 代码本身。

7.1. Python 在 iOS 上的运行时

7.1.1. iOS 版本兼容性

最低支持的 iOS 版本在编译时通过 configure--host 选项指定。默认情况下,为 iOS 编译时,Python 将以最低支持 iOS 13.0 版本进行编译。要使用不同的最低 iOS 版本,请在 --host 参数中提供版本号——例如,--host=arm64-apple-ios15.4-simulator 将编译一个部署目标为 15.4 的 ARM64 模拟器版本。

7.1.2. 平台标识

在 iOS 上执行时,sys.platform 将报告为 ios。无论应用是在模拟器还是物理设备上运行,在 iPhone 或 iPad 上都会返回此值。

有关特定运行时环境的信息,包括 iOS 版本、设备型号以及设备是否为模拟器,可以使用 platform.ios_ver() 获取。platform.system() 将根据设备报告 iOSiPadOS

os.uname() 报告内核级别的详细信息;它将报告一个名为 Darwin 的名称。

7.1.3. 标准库可用性

Python 标准库在 iOS 上有一些显著的遗漏和限制。详情请参阅iOS 的 API 可用性指南

7.1.4. 二进制扩展模块

iOS 平台的一个显著不同之处在于,App Store 分发对应用程序的打包有严格的要求。其中一个要求规定了二进制扩展模块的分发方式。

iOS App Store 要求 iOS 应用中的*所有*二进制模块都必须是动态库,包含在一个带有适当元数据的框架中,并存储在打包应用的 Frameworks 文件夹中。每个框架只能有一个二进制文件,并且在 Frameworks 文件夹之外不能有任何可执行的二进制材料。

这与 Python 通常的分发二进制文件的方式相冲突,后者允许从 sys.path 上的任何位置加载二进制扩展模块。为确保符合 App Store 政策,iOS 项目必须对任何 Python 包进行后处理,将 .so 二进制模块转换为带有适当元数据和签名的独立框架。有关如何执行此后处理的详细信息,请参阅将 Python 添加到您的项目的指南。

为了帮助 Python 在新位置发现二进制文件,sys.path 上的原始 .so 文件被替换为 .fwork 文件。此文件是一个文本文件,包含相对于应用包的框架二进制文件的位置。为了让框架能够解析回原始位置,框架必须包含一个 .origin 文件,其中包含相对于应用包的 .fwork 文件的位置。

例如,考虑导入 from foo.bar import _whiz 的情况,其中 _whiz 由二进制模块 sources/foo/bar/_whiz.abi3.so 实现,sources 是在 sys.path 上注册的相对于应用程序包的位置。此模块*必须*作为 Frameworks/foo.bar._whiz.framework/foo.bar._whiz 分发(框架名称由模块的完整导入路径创建),并在 .framework 目录中有一个 Info.plist 文件,将二进制文件标识为框架。 foo.bar._whiz 模块将在原始位置由一个 sources/foo/bar/_whiz.abi3.fwork 标记文件表示,其中包含路径 Frameworks/foo.bar._whiz/foo.bar._whiz。该框架还将包含 Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin,其中包含 .fwork 文件的路径。

在 iOS 上运行时,Python 解释器将安装一个 AppleFrameworkLoader,它能够读取和导入 .fwork 文件。导入后,二进制模块的 __file__ 属性将报告为 .fwork 文件的位置。但是,已加载模块的 ModuleSpec 将报告 origin 为框架文件夹中二进制文件的位置。

7.1.5. 编译器存根二进制文件

Xcode 没有为 iOS 显式暴露编译器;相反,它使用一个 xcrun 脚本来解析完整的编译器路径(例如,xcrun --sdk iphoneos clang 来获取 iPhone 设备的 clang)。然而,使用此脚本会带来两个问题:

  • xcrun 的输出包含特定于机器的路径,导致 sysconfig 模块无法在用户之间共享;以及

  • 它会导致 CC/CPP/LD/AR 定义中包含空格。许多 C 生态系统工具都假定可以在第一个空格处分割命令行以获取编译器可执行文件的路径;使用 xcrun 时情况并非如此。

为了避免这些问题,Python 为这些工具提供了存根。这些存根是围绕底层 xcrun 工具的 shell 脚本包装器,分布在与已编译的 iOS 框架一起分发的 bin 文件夹中。这些脚本是可重定位的,并且将始终解析为适当的本地系统路径。通过将这些脚本包含在框架附带的 bin 文件夹中,sysconfig 模块的内容对于最终用户编译自己的模块变得有用。在为 iOS 编译第三方 Python 模块时,应确保这些存根二进制文件在您的路径中。

7.2. 在 iOS 上安装 Python

7.2.1. 构建 iOS 应用的工具

为 iOS 构建需要使用 Apple 的 Xcode 工具。强烈建议您使用最新稳定版的 Xcode。这将需要使用最新(或次新)发布的 macOS 版本,因为 Apple 不会为旧版 macOS 维护 Xcode。Xcode 命令行工具不足以进行 iOS 开发;您需要*完整*的 Xcode 安装。

如果您想在 iOS 模拟器上运行代码,您还需要安装一个 iOS 模拟器平台。当您首次运行 Xcode 时,应该会提示您选择一个 iOS 模拟器平台。或者,您可以通过在 Xcode 设置面板的 Platforms 选项卡中选择来添加 iOS 模拟器平台。

7.2.2. 将 Python 添加到 iOS 项目

Python 可以添加到任何 iOS 项目中,使用 Swift 或 Objective-C。以下示例将使用 Objective-C;如果您使用 Swift,您可能会发现像 PythonKit 这样的库会很有帮助。

要将 Python 添加到 iOS Xcode 项目中:

  1. 构建或获取一个 Python XCFramework。有关如何构建 Python XCFramework 的详细信息,请参阅 Apple/iOS/README.md(在 CPython 源代码分发中)的说明。您至少需要一个支持 arm64-apple-ios 的构建,外加 arm64-apple-ios-simulatorx86_64-apple-ios-simulator 中的一个。

  2. XCframework 拖入您的 iOS 项目。在下面的说明中,我们假设您已将 XCframework 放入项目的根目录;但是,您可以通过调整路径来使用任何其他位置。

  3. 将您的应用程序代码作为文件夹添加到您的 Xcode 项目中。在下面的说明中,我们假设您的用户代码位于项目根目录下一个名为 app 的文件夹中;您可以通过调整路径来使用任何其他位置。确保此文件夹与您的应用目标相关联。

  4. 通过选择 Xcode 项目的根节点,然后在出现的侧边栏中选择目标名称来选择应用目标。

  5. 在“General”设置中的“Frameworks, Libraries and Embedded Content”下,添加 Python.xcramework,并选择“Embed & Sign”。

  6. 在“Build Settings”选项卡中,修改以下内容:

    • Build Options

      • User Script Sandboxing: No

      • Enable Testability: Yes

    • Search Paths

      • Framework Search Paths: $(PROJECT_DIR)

      • Header Search Paths: "$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"

    • Apple Clang - Warnings - All languages

      • Quoted Include In Framework Header: No

  7. 添加一个构建步骤来处理 Python 标准库和您自己的 Python 二进制依赖项。在“Build Phases”选项卡中,在“Embed Frameworks”步骤*之前*,但在“Copy Bundle Resources”步骤*之后*添加一个新的“Run Script”构建步骤。将该步骤命名为“Process Python libraries”,禁用“Based on dependency analysis”复选框,并将脚本内容设置为:

    set -e
    source $PROJECT_DIR/Python.xcframework/build/build_utils.sh
    install_python Python.xcframework app
    

    如果您将 XCframework 放置在项目根目录以外的位置,请修改第一个参数的路径。

  8. 添加 Objective-C 代码以在嵌入模式下初始化和使用 Python 解释器。您应确保:

    您的应用包位置可以使用 [[NSBundle mainBundle] resourcePath] 来确定。

这些说明的第 7 步和第 8 步假设您有一个名为 app 的纯 Python 应用程序代码的单个文件夹。如果您的应用中有第三方二进制模块,则需要一些额外的步骤:

  • 您需要确保任何包含第三方二进制文件的文件夹都与应用目标关联,或者在第 7 步中明确复制。第 7 步还应清除不适用于特定构建目标平台的任何二进制文件(即,如果您正在构建针对模拟器的应用,则删除任何设备二进制文件)。

  • 如果您正在为第三方包使用单独的文件夹,请确保在第 7 步中将该文件夹添加到对 install_python 的调用末尾,并在第 8 步中作为 PYTHONPATH 配置的一部分。

  • 如果任何包含第三方包的文件夹将包含 .pth 文件,您应将该文件夹添加为*站点目录*(使用 site.addsitedir()),而不是直接添加到 PYTHONPATHsys.path

7.2.3. 测试 Python 包

CPython 源代码树包含一个测试平台项目,用于在 iOS 模拟器上运行 CPython 测试套件。该测试平台也可用作在 iOS 上运行您的 Python 库测试套件的测试平台项目。

在构建或获取 iOS XCFramework(详情请参阅 Apple/iOS/README.md)后,创建 Python iOS 测试平台项目的一个克隆。如果您使用 Apple 构建脚本来构建 XCframework,您可以运行:

$ python cross-build/iOS/testbed clone --app <path/to/module1> --app <path/to/module2> app-testbed

或者,如果您已经获得了自己的 XCframework,则通过运行:

$ python Apple/testbed clone --platform iOS --framework <path/to/Python.xcframework> --app <path/to/module1> --app <path/to/module2> app-testbed

任何使用 --app 标志指定的文件夹都将被复制到克隆的测试平台项目中。最终的测试平台将在 app-testbed 文件夹中创建。在此示例中,module1module2 将是运行时可导入的模块。如果您的项目有其他依赖项,可以将它们安装到 app-testbed/Testbed/app_packages 文件夹中(使用 pip install --target app-testbed/Testbed/app_packages 或类似命令)。

然后,您可以使用 app-testbed 文件夹来运行您应用的测试套件。例如,如果 module1.tests 是您测试套件的入口点,您可以运行:

$ python app-testbed run -- module1.tests

这相当于在桌面 Python 构建上运行 python -m module1.tests。在 -- 之后的所有参数将作为 python -m 在桌面机器上的参数传递给测试平台。

您还可以通过运行以下命令在 Xcode 中打开测试平台项目:

$ open app-testbed/iOSTestbed.xcodeproj

这将允许您使用完整的 Xcode 工具套件进行调试。

用于运行测试套件的参数是作为测试计划的一部分定义的。要修改测试计划,请选择项目树的测试计划节点(它应该是根节点的第一个子节点),然后选择“Configurations”选项卡。修改“Arguments Passed On Launch”值以更改测试参数。

测试计划还禁用了并行测试,并指定使用 Testbed.lldbinit 文件来提供调试器的配置。默认的调试器配置禁用了对 SIGINTSIGUSR1SIGUSR2SIGXFSZ 信号的自动断点。

7.3. App Store 合规性

向第三方 iOS 设备分发应用的唯一机制是向 iOS App Store 提交应用;提交分发的应用必须通过 Apple 的应用审核流程。此流程包括一组自动化验证规则,用于检查提交的应用程序包中是否存在有问题的代码。必须采取一些步骤来确保您的应用能够通过这些验证步骤。

7.3.1. 标准库中的不兼容代码

Python 标准库包含一些已知违反这些自动化规则的代码。虽然这些违规似乎是误报,但 Apple 的审核规则是无法挑战的;因此,有必要修改 Python 标准库,以使应用能够通过 App Store 审核。

Python 源代码树包含一个补丁文件,它将删除所有已知会导致 App Store 审核流程问题的代码。在为 iOS 构建时,此补丁会自动应用。

7.3.2. 隐私清单

2025 年 4 月,Apple 引入了一项要求,要求某些第三方库提供隐私清单。因此,如果您的二进制模块使用了受影响的库之一,您必须为该库提供一个 .xcprivacy 文件。OpenSSL 是受此要求影响的库之一,但还有其他库。

如果您生成一个名为 mymodule.so 的二进制模块,并使用上述第 7 步中描述的 Xcode 构建脚本,您可以将一个 mymodule.xcprivacy 文件放在 mymodule.so 旁边,当二进制模块被转换为框架时,隐私清单将被安装到所需的位置。