Following system colour scheme - Python 增強提案 Selected dark colour scheme - Python 增強提案 Selected light colour scheme - Python 增強提案

Python 增強提案 (Python Enhancement Proposals)

PEP 730 – 將 iOS 加入支援平台

作者:
Russell Keith-Magee <russell at keith-magee.com>
贊助人:
Ned Deily <nad at python.org>
討論於:
Discourse 討論串
狀態:
最終 (Final)
類型:
標準軌跡 (Standards Track)
建立日期:
2023 年 10 月 9 日
Python 版本:
3.13
決議:
Discourse 訊息

目錄

重要資訊

本 PEP 為歷史文件。最新且標準化的說明文件現可見於 在 iOS 上使用 Python

×

關於如何提出變更建議,請參閱 PEP 1

摘要

本 PEP 提議將 iOS 加入 CPython 的支援平台中。初步目標是在 Python 3.13 中達成 Tier 3 支援。本 PEP 描述了支援 iOS 所需的技術變更,並說明了將 iOS 納入 Tier 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 上則支援 arm64x86_64

與 macOS 一樣,iOS 支援建立包含多種 CPU 架構的「胖」二進位檔 (fat binaries)。然而,胖二進位檔不能跨越不同的 ABI。亦即,可以擁有一個胖的模擬器二進位檔或一個胖的裝置二進位檔,但無法建立一個同時滿足模擬器與裝置需求的單一胖「iOS」二進位檔。為了支援單一開發產物的發布,Apple 使用「XCframework」結構——這是一個封裝多個 ABI 並實作共同 API 的封裝器。

iOS 與 macOS 一樣運行在 Darwin 核心上。然而,由於 iOS 與 macOS 之間存在顯著的平台差異,因此在實作層級上有必要區分兩者。

iOS 程式碼是為了相容於最低 iOS 版本而編譯的。

Apple 在行銷資料中經常提到「iPadOS」。然而,從開發的角度來看,iPadOS 與 iOS 之間沒有明顯差異。為 iphoneosiphonesimulator ABI 編譯的二進位檔可以部署在 iPad 上。

其他 Apple 平台(如 tvOS、watchOS 和 visionOS)使用不同的 ABI,不在本 PEP 的涵蓋範圍內。

POSIX 相容性

iOS 大體上是一個 POSIX 平台。然而,與 WASI/Emscripten 類似,iOS 上存在一些無法使用的 POSIX API,也有一些完全不存在的 POSIX API。

其中最顯著的是 iOS 不提供任何形式的多程序支援。iOS API 中確實存在 forkspawn;然而,若呼叫這些函式,呼叫的 iOS 程序會停止,且新程序不會啟動。

與 WASI/Emscripten 不同,iOS 確實支援執行緒。

Socket 處理也存在顯著限制。由於程序的沙盒機制,無法透過 Socket 進行程序間通訊。不過,用於網路通訊的 Socket 可用的。

動態函式庫

iOS 的 App Store 審核指南允許應用程式使用 Objective C 或 Swift 以外的語言編寫。但是,它們對提交發布的應用程式結構有非常嚴格的規範。

iOS 應用程式可以使用動態載入的函式庫;但對於動態載入內容如何封裝以在 iOS 上使用,有非常嚴格的要求。

  • 動態二進位內容必須編譯為動態函式庫,而非共享物件或二進位套件 (binary bundles)。
  • 它們必須以 Framework 的形式封裝在應用程式套件中。
  • 每個 Framework 只能包含一個動態函式庫。
  • Framework 必須包含在 iOS 應用程式的 Frameworks 資料夾中。
  • Framework 不得包含任何非函式庫的內容。

這對 CPython 的運作施加了一些限制。無法將二進位模組儲存在 lib-dynloadsite-packages 資料夾中;它們必須儲存在應用程式的 Frameworks 資料夾內,且每個模組都必須封裝在一個 Framework 中。這也意味著「Python 模組可以透過自身的 __file__ 屬性建構二進位模組位置」這一常見假設不再成立。

與 macOS 一樣,要編譯一個可從靜態連結的 Python 建構中存取的二進位模組,需要使用 --undefined dynamic_lookup 選項,以避免將 libpython3.x 連結到每個二進位模組中。然而在 iOS 上,當使用此編譯器旗標時,會引發棄用警告。在 macOS 上也觀察到了此旗標引發的警告,但 Apple 的回應暗示他們 不打算透過移除此選項來破壞 CPython 生態系統。由於 Python 目前在 iOS 上尚未有顯著的影響力,很難判斷 iOS 對此旗標的使用是否屬於同樣的情況。

主控台與互動式使用

發布傳統的 CPython REPL 或互動式「python.exe」不應視為此項工作的目標。

行動裝置(包括 iOS)不提供 TTY 風格的主控台。它們不提供 stdinstdoutstderr。iOS 提供系統日誌,可以安裝重新導向,將所有 stdoutstderr 的內容重新導向至系統日誌;但沒有 stdin 的對應物。

此外,iOS 對於在執行階段下載額外程式碼有限制(因為這種行為在功能上與試圖繞過 App Store 審核無異)。因此,傳統的「建立虛擬環境並使用 pip install」的開發體驗在 iOS 上將不可行。

可以建構一個提供 REPL 介面的原生 iOS 應用程式。這將更接近 IDLE 風格的使用者體驗;然而,Tkinter 無法在 iOS 上使用,因此任何應用程式都需要徹底重寫。iOS App Store 中已包含多個此類應用程式的範例(例如 PythonistaPyto)。這項工作的重點在於提供一種嵌入式發布,供 IDE 風格的原生介面使用,而非提供一個 Python 的 iOS 使用者介面「應用程式」。

規範

平台識別

sys

sys.platform 在模擬器與實體裝置上皆會識別為 "ios"

sys.implementation._multiarch 將描述 ABI 與 CPU 架構

  • ARM64 裝置為 "arm64-iphoneos"
  • ARM64 模擬器為 "arm64-iphonesimulator"
  • x86_64 模擬器為 "x86_64-iphonesimulator"

platform

platform 將被修改以支援回傳 iOS 特定的詳細資訊。大多數由 platform 模組回傳的值將與 os.uname() 回傳的值相符,但以下除外:

  • platform.system() - 回傳 "iOS"iPadOS(取決於所使用的硬體),而非 "Darwin"
  • platform.release() - 回傳字串格式的 iOS 版本號(例如 "16.6.1"),而非 Darwin 核心版本。

此外,將新增一個 platform.ios_ver() 方法。這與可用於提供 macOS 版本資訊的 platform.mac_ver() 相呼應。ios_ver() 將回傳一個包含下列內容的 namedtuple:

  • system - 作業系統名稱(iOSiPadOS,視硬體而定)
  • 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.platformsys.implementation._multiarch 等)來建構識別碼。

子程序支援

iOS 將利用 WASI/Emscripten 所建立的禁用子程序模式。subprocess 模組在嘗試啟動子程序時將引發例外,且 os.forkos.spawn 呼叫將引發 OSError

動態模組載入

為了適應 iOS 的動態載入,importlib 引導程式 (bootstrap) 將進行擴充,加入一個能夠將 Python 二進位模組請求轉換為 Framework 位置的 metapath finder。此 finder 僅會在 sys.platform == "ios" 時安裝。

此 finder 將透過使用完整模組名稱作為框架名稱(即 foo.bar._whiz.framework),將 Python 模組名稱(例如 foo.bar._whiz)轉換為唯一的 Framework 名稱。Framework 是一個目錄;finder 將在該目錄中尋找名為 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 流程將產生一個可用於單一 ABI 與架構的 Python.framework 產物。

還需要額外的工具來將多種架構的 Python.framework 建構合併為單一「胖」函式庫。同樣需要工具將多個 ABI 合併為 Apple 用於在單一套件中分發不同 ABI 多個 Framework 的 XCframework 格式。

將提供一個 Xcode 專案,用於執行 CPython 測試套件。將提供工具以自動化編譯測試套件二進位檔、啟動模擬器、安裝測試套件並執行它的過程。

發布

將 iOS 加入 Tier 3 平台僅需支援從未經修補的 CPython 程式碼檢出版本編譯出 iOS 相容的建構。這不需要製作供終端使用者使用的正式發布版 iOS 產物。

若 iOS 更新至 Tier 2 或 1 支援,則可用於產生 XCframework 套件的工具即可用於產生 iOS 發布產物。這之後可以作為類比於 Windows 嵌入式發布的「嵌入式發布」發布,或是作為可加入 Xcode 專案的 CocoaPod 或 Swift 套件。

CI 資源

Anaconda 已提議提供實體硬體以執行 iOS buildbots。

GitHub Actions 能夠在其 macOS 機器上託管 iOS 模擬器,且 iOS 模擬器可由指令碼環境控制。免費方案目前僅提供 x86_64 macOS 機器;然而,ARM64 執行器已在付費方案中提供。不過,為了避免耗盡 macOS 執行器資源,iOS 的 GitHub Actions 執行流程不會被加入標準 CI 設定中。

套件封裝 (Packaging)

iOS 不會提供「通用」wheel 格式。相反地,每個 ABI-架構組合都將提供各自的 wheel。

iOS wheel 將使用下列標籤:

  • ios_12_0_arm64_iphoneos
  • ios_12_0_arm64_iphonesimulator
  • ios_12_0_x86_64_iphonesimulator

在這些標籤中,「12.0」是支援的最低 iOS 版本。與 macOS 一樣,標籤將包含編譯 wheel 時所選的最低 iOS 版本;例如,若編譯時的最低 iOS 版本為 15.0,則會使用 ios_15_0_* 標籤。在撰寫本文時,iOS 12.0 展現了大多數重要的 iOS 功能,同時涵蓋近 100% 的裝置;這將作為 iOS 版本匹配的基準。

這些 wheel 可以原地 (in-situ) 包含二進位模組(即與 Python 原始碼放在一起,就像桌面平台的 wheel 一樣);然而,它們需要進行後處理,因為二進位模組必須移動到「Frameworks」位置才能發布。這可以使用 Xcode 建構步驟自動完成。

PEP 11 更新

PEP 11 將更新以包含兩個 iOS ABI:

  • arm64-apple-ios
  • arm64-apple-ios-simulator

Ned Deily 將擔任這些 ABI 的核心團隊聯絡人。

x86_64-apple-ios-simulator 目標將以盡力而為 (best-effort) 的方式支援,但不會作為 Tier 3 支援的目標。這是由於 x86_64 作為模擬平台的即將棄用,以及目前委託 x86_64 macOS 硬體的困難度。

回溯相容性

新增一個平台不會對 CPython 本身引入任何向後相容性的疑慮。

對於歷史上一直提供 CPython 支援的專案(即 BeeWare 與 Kivy),如果任何 CPython 修補程式的最終形式與他們歷史上使用的修補程式不一致,可能會產生一些向後相容性的影響。

雖然嚴格來說不是向後相容性問題,但確實有平台採用的考量。儘管 CPython 本身可能支援 iOS,但如果如何產生 iOS 相容的 wheel 不明確,且諸如 cryptography、Pillow 和 NumPy 等著名函式庫不提供 iOS wheel,社群在 iOS 上採用 Python 的能力將會受到限制。因此,有必要清楚記錄專案如何將 iOS 建構加入其 CI 與發布工具中。將 iOS 支援加入如 crossenvcibuildwheel 等工具可能是達成此目標的一種方法。

安全性影響

將 iOS 加入為新平台不會增加任何安全性影響。

如何教學

與本 PEP 相關的教育需求主要與終端使用者如何將 iOS 支援加入他們自己的 Xcode 專案有關。這可以透過該過程的說明文件與教學來完成。如果/當支援等級從 Tier 3 提升至 Tier 2 或 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,可由任何平台實作,例如為了區分在 ARM64 硬體上執行 x86_64 程式碼,或在 QEMU 或其他虛擬化方法中執行時的情況。這遭到了拒絕,理由是不清楚「模擬器」的一致解釋為何,或者如何在 iOS 案例之外偵測模擬器。

最終決定將此細節保持為 iOS 特定,並將其包含在 platform.ios_ver() API 中。

GNU 編譯器三元組 (GNU compiler triples)

autoconf 要求使用 GNU 編譯器三元組來識別建構與主機平台。然而,autoconf 工具鏈不提供對 iOS 模擬器的原生支援,因此我們必須設法將 iOS 硬體塞進 GNU 的命名規範中。

這可以做到(透過一些對 config.sub 的修補),但這導致了 2 個主要的命名不一致來源:

  • arm64aarch64 作為 64 位元 ARM 硬體的識別碼;以及
  • 使用什麼識別碼來代表模擬器。

Apple 自己的工具使用 arm64 作為架構,但在某些情況下似乎對 aarch64 有所容忍。裝置平台識別為 iphoneosiphonesimulator

Rust 工具鏈使用 aarch64 作為架構,並使用 aarch64-apple-iosaarch64-apple-ios-sim 來識別裝置平台;然而,它們使用 x86_64-apple-ios 來代表 x86_64 硬體上的 iOS 模擬器

最終決定使用 arm64-apple-iosarm64-apple-ios-simulator,原因如下:

  1. autoconf 工具鏈在 config.sub 中已經包含了對 ios 作為平台的支援;只有模擬器沒有代表。
  2. 主機三元組的第三部分用作 sys.platform
  3. 當 Apple 自己的工具參考 CPU 架構時,它們使用 arm64,且 GNU 工具對架構的使用在建構過程之外是不可見的。
  4. 當 Apple 自己的工具獨立於 OS 參考模擬器狀態時(例如在 Swift 子模組的命名中),它們使用 -simulator 後綴。
  5. 雖然某些 iOS 套件會使用 Rust,但所有 iOS 套件都會使用 Apple 的工具。

本文件的最初接受版本使用 aarch64 形式作為 PEP 11 識別碼;這在定稿時已更正。

「通用」wheel 格式

macOS 目前支援 2 種 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 要麼撤銷棄用決定,要麼永遠不完成該棄用。

鑑於 Apple 的決策過程完全不透明,這頂多是一個有風險的選擇。結合更廣泛的 iOS 開發生態系統鼓勵使用 Framework 的事實,沒有需要考慮的靜態函式庫舊有用途,且靜態連結的 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 buildbot 可能會有困難。在 ARM64 硬體上以 x86_64 相容模式執行 macOS 二進位檔是可能的;然而,這對於測試目的並不理想。因此,x86_64 模擬器 (x86_64-apple-ios-simulator) 將不會作為 Tier 3 目標加入。極有可能的是,iOS 支援將在 x86_64 上無需任何修改即可運作;這僅影響官方的 Tier 3 地位。

裝置端測試

模擬器上的 CI 測試可以相當容易地適應。裝置端測試則困難得多,因為可配置以提供 Buildbots 或 Github Actions 執行器的裝置農場 (device farms) 可用性有限。

然而,裝置端測試可能並非必要。作為一個數據點,Apple 的 Xcode Cloud 解決方案不提供裝置端測試。他們依賴於 API 在裝置與模擬器之間是一致的這一事實,且 ARM64 模擬器測試足以揭露 CPU 特有的問題。

_multiarch 標籤的排序

本文件的最初接受版本對 sys.implementation._multiarch(與相關值,如 wheel 標籤)使用了 <平台>-<架構> 排序(例如 iphoneos-arm64)。最終合併的版本使用了 <架構>-<平台> 排序(例如 arm64-iphoneos)。這是為了與其他平台上(特別是 Linux)的編譯器三元組保持一致,這些平台在作業系統之前指定了架構。

platform.ios_ver() 的回傳值

本文件的最初接受版本沒有包含 system 識別碼。這是為了支援 platform.system() 的實作,而在實作階段加入的。

本文件的最初接受版本還描述了 min_release 將會在 ios_ver() 結果中回傳。最終版本省略了 min_release 值,因為它在執行階段並不重要;它僅影響二進位相容性。最低版本確實包含在 sysconfig.get_platform() 所回傳的值中,因為這用於定義 wheel(與其他二進位檔)的相容性。


來源:https://github.com/python/peps/blob/main/peps/pep-0730.rst

最後修改:2025-02-01 08:55:40 GMT