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 旁邊,當二進位制模組被轉換為框架時,隱私清單將被安裝到所需的位置。