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 加入 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 上則支援 arm64 與 x86_64。
與 macOS 一樣,iOS 支援建立包含多種 CPU 架構的「胖」二進位檔 (fat binaries)。然而,胖二進位檔不能跨越不同的 ABI。亦即,可以擁有一個胖的模擬器二進位檔或一個胖的裝置二進位檔,但無法建立一個同時滿足模擬器與裝置需求的單一胖「iOS」二進位檔。為了支援單一開發產物的發布,Apple 使用「XCframework」結構——這是一個封裝多個 ABI 並實作共同 API 的封裝器。
iOS 與 macOS 一樣運行在 Darwin 核心上。然而,由於 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 不提供任何形式的多程序支援。iOS API 中確實存在 fork 與 spawn;然而,若呼叫這些函式,呼叫的 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-dynload 或 site-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 風格的主控台。它們不提供 stdin、stdout 或 stderr。iOS 提供系統日誌,可以安裝重新導向,將所有 stdout 和 stderr 的內容重新導向至系統日誌;但沒有 stdin 的對應物。
此外,iOS 對於在執行階段下載額外程式碼有限制(因為這種行為在功能上與試圖繞過 App Store 審核無異)。因此,傳統的「建立虛擬環境並使用 pip install」的開發體驗在 iOS 上將不可行。
可以建構一個提供 REPL 介面的原生 iOS 應用程式。這將更接近 IDLE 風格的使用者體驗;然而,Tkinter 無法在 iOS 上使用,因此任何應用程式都需要徹底重寫。iOS App Store 中已包含多個此類應用程式的範例(例如 Pythonista 與 Pyto)。這項工作的重點在於提供一種嵌入式發布,供 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- 作業系統名稱(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 引導程式 (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_iphoneosios_12_0_arm64_iphonesimulatorios_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-iosarm64-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 支援加入如 crossenv 與 cibuildwheel 等工具可能是達成此目標的一種方法。
安全性影響
將 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 個主要的命名不一致來源:
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 自己的工具獨立於 OS 參考模擬器狀態時(例如在 Swift 子模組的命名中),它們使用
-simulator後綴。 - 雖然某些 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 特有的問題。
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