7. 在 iOS 上使用 Python¶
- 作者:
Russell Keith-Magee (2024-03)
iOS 上的 Python 與桌面平臺上的 Python 不同。在桌面平臺上,Python 通常作為系統資源安裝,可供該計算機的任何使用者使用。然後,使用者透過執行 python 可執行檔案並在互動式提示符下輸入命令或執行 Python 指令碼來與 Python 互動。
在 iOS 上,沒有作為系統資源安裝的概念。唯一的軟體分發單元是“應用程式”。也沒有控制檯可以執行 python 可執行檔案或與 Python REPL 互動。
因此,在 iOS 上使用 Python 的唯一方法是在嵌入模式下 - 即,透過編寫原生 iOS 應用程式,並使用 libPython
嵌入 Python 直譯器,並使用 Python 嵌入 API 呼叫 Python 程式碼。完整的 Python 直譯器、標準庫和所有 Python 程式碼隨後打包成一個獨立的捆綁包,可以透過 iOS App Store 分發。
如果您是第一次嘗試用 Python 編寫 iOS 應用程式,那麼 BeeWare 和 Kivy 等專案將提供更易於上手的使用體驗。這些專案管理與執行 iOS 專案相關的複雜性,因此您只需要處理 Python 程式碼本身。
7.1. iOS 上的 Python 執行時¶
7.1.1. iOS 版本相容性¶
最低支援的 iOS 版本在編譯時指定,使用 --host
選項到 configure
。預設情況下,在為 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()
將根據裝置報告 iOS
或 iPadOS
。
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 設定面板的“平臺”選項卡中新增 iOS 模擬器平臺。
7.2.2. 將 Python 新增到 iOS 專案¶
可以使用 Swift 或 Objective C 將 Python 新增到任何 iOS 專案。以下示例將使用 Objective C;如果你使用 Swift,你可能會發現像 PythonKit 這樣的庫很有幫助。
將 Python 新增到 iOS Xcode 專案
構建或獲取 Python
XCFramework
。有關如何構建 PythonXCFramework
的詳細資訊,請參閱 iOS/README.rst(在 CPython 原始碼發行版中)中的說明。至少,你需要一個支援arm64-apple-ios
的構建,以及arm64-apple-ios-simulator
或x86_64-apple-ios-simulator
中的一個。將
XCframework
拖到你的 iOS 專案中。在以下說明中,我們將假設你已將XCframework
放入專案的根目錄中;但是,你可以透過根據需要調整路徑來使用你想要的任何其他位置。將
iOS/Resources/dylib-Info-template.plist
檔案拖到你的專案中,並確保它與應用程式目標相關聯。將你的應用程式程式碼作為資料夾新增到你的 Xcode 專案中。在以下說明中,我們將假設你的使用者程式碼位於專案根目錄中名為
app
的資料夾中;你可以透過根據需要調整路徑來使用任何其他位置。確保此資料夾與你的應用程式目標相關聯。透過選擇 Xcode 專案的根節點,然後在出現的側邊欄中選擇目標名稱來選擇應用程式目標。
在“常規”設定的“框架、庫和嵌入內容”下,新增
Python.xcframework
,並選擇“嵌入並簽名”。在“構建設定”選項卡中,修改以下內容
構建選項
使用者指令碼沙盒:否
啟用可測試性:是
搜尋路徑
框架搜尋路徑:
$(PROJECT_DIR)
標頭搜尋路徑:
"$(BUILT_PRODUCTS_DIR)/Python.framework/Headers"
Apple Clang - 警告 - 所有語言
框架標頭中帶引號的包含:否
新增一個構建步驟,將 Python 標準庫複製到你的應用程式中。在“構建階段”選項卡中,在 “嵌入框架”步驟之前,但在“複製捆綁資源”步驟之後新增新的“執行指令碼”構建步驟。將該步驟命名為“安裝特定於目標的 Python 標準庫”,停用“基於依賴關係分析”複選框,並將指令碼內容設定為
set -e mkdir -p "$CODESIGNING_FOLDER_PATH/python/lib" if [ "$EFFECTIVE_PLATFORM_NAME" = "-iphonesimulator" ]; then echo "Installing Python modules for iOS Simulator" rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64_x86_64-simulator/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" else echo "Installing Python modules for iOS Device" rsync -au --delete "$PROJECT_DIR/Python.xcframework/ios-arm64/lib/" "$CODESIGNING_FOLDER_PATH/python/lib/" fi
請注意,XCframework 中模擬器“切片”的名稱可能不同,具體取決於你的
XCFramework
支援的 CPU 架構。新增第二個構建步驟,將標準庫中的二進位制擴充套件模組處理為“框架”格式。在你在步驟 8 中新增的步驟之後直接新增一個名為“準備 Python 二進位制模組”的“執行指令碼”構建步驟。它還應取消選中“基於依賴關係分析”,幷包含以下指令碼內容
set -e install_dylib () { INSTALL_BASE=$1 FULL_EXT=$2 # The name of the extension file EXT=$(basename "$FULL_EXT") # The location of the extension file, relative to the bundle RELATIVE_EXT=${FULL_EXT#$CODESIGNING_FOLDER_PATH/} # The path to the extension file, relative to the install base PYTHON_EXT=${RELATIVE_EXT/$INSTALL_BASE/} # The full dotted name of the extension module, constructed from the file path. FULL_MODULE_NAME=$(echo $PYTHON_EXT | cut -d "." -f 1 | tr "/" "."); # A bundle identifier; not actually used, but required by Xcode framework packaging FRAMEWORK_BUNDLE_ID=$(echo $PRODUCT_BUNDLE_IDENTIFIER.$FULL_MODULE_NAME | tr "_" "-") # The name of the framework folder. FRAMEWORK_FOLDER="Frameworks/$FULL_MODULE_NAME.framework" # If the framework folder doesn't exist, create it. if [ ! -d "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" ]; then echo "Creating framework for $RELATIVE_EXT" mkdir -p "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER" cp "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" plutil -replace CFBundleExecutable -string "$FULL_MODULE_NAME" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" plutil -replace CFBundleIdentifier -string "$FRAMEWORK_BUNDLE_ID" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/Info.plist" fi echo "Installing binary for $FRAMEWORK_FOLDER/$FULL_MODULE_NAME" mv "$FULL_EXT" "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" # Create a placeholder .fwork file where the .so was echo "$FRAMEWORK_FOLDER/$FULL_MODULE_NAME" > ${FULL_EXT%.so}.fwork # Create a back reference to the .so file location in the framework echo "${RELATIVE_EXT%.so}.fwork" > "$CODESIGNING_FOLDER_PATH/$FRAMEWORK_FOLDER/$FULL_MODULE_NAME.origin" } PYTHON_VER=$(ls -1 "$CODESIGNING_FOLDER_PATH/python/lib") echo "Install Python $PYTHON_VER standard library extension modules..." find "$CODESIGNING_FOLDER_PATH/python/lib/$PYTHON_VER/lib-dynload" -name "*.so" | while read FULL_EXT; do install_dylib python/lib/$PYTHON_VER/lib-dynload/ "$FULL_EXT" done # Clean up dylib template rm -f "$CODESIGNING_FOLDER_PATH/dylib-Info-template.plist" echo "Signing frameworks as $EXPANDED_CODE_SIGN_IDENTITY_NAME ($EXPANDED_CODE_SIGN_IDENTITY)..." find "$CODESIGNING_FOLDER_PATH/Frameworks" -name "*.framework" -exec /usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" ${OTHER_CODE_SIGN_FLAGS:-} -o runtime --timestamp=none --preserve-metadata=identifier,entitlements,flags --generate-entitlement-der "{}" \;
新增 Objective C 程式碼以初始化並在嵌入模式下使用 Python 直譯器。你應該確保
UTF-8 模式(
PyPreConfig.utf8_mode
)已啟用;緩衝 stdio(
PyConfig.buffered_stdio
)已停用;寫入位元組碼(
PyConfig.write_bytecode
)已停用;訊號處理程式(
PyConfig.install_signal_handlers
)已啟用;系統日誌記錄(
PyConfig.use_system_logger
)已啟用(可選,但強烈建議);直譯器的
PYTHONHOME
配置為指向你應用程式捆綁包的python
子資料夾;並且直譯器的
PYTHONPATH
包括
你應用程式捆綁包的
python/lib/python3.X
子資料夾,你應用程式捆綁包的
python/lib/python3.X/lib-dynload
子資料夾,以及你應用程式捆綁包的
app
子資料夾可以使用
[[NSBundle mainBundle] resourcePath]
確定你應用程式的捆綁包位置。
這些說明的步驟 8、9 和 10 假設你有一個名為 app
的純 Python 應用程式程式碼的單個資料夾。如果你的應用程式中有第三方二進位制模組,則需要一些額外的步驟
你需要確保任何包含第三方二進位制檔案的資料夾要麼與應用程式目標相關聯,要麼作為步驟 8 的一部分複製進來。步驟 8 還應清除任何不適合特定構建目標平臺的二進位制檔案(即,如果你正在構建以模擬器為目標的應用程式,則刪除所有裝置二進位制檔案)。
任何包含第三方二進位制檔案的資料夾都必須由步驟 9 處理成框架形式。可以複製和調整處理
lib-dynload
資料夾的install_dylib
的呼叫以實現此目的。如果你使用單獨的資料夾來存放第三方包,請確保該資料夾包含在步驟 10 中的
PYTHONPATH
配置中。
7.2.3. 測試 Python 包¶
CPython 原始碼樹包含一個 測試平臺專案,用於在 iOS 模擬器上執行 CPython 測試套件。此測試平臺也可以用作在 iOS 上執行你的 Python 庫測試套件的測試平臺專案。
在構建或獲取 iOS XCFramework 後(有關詳細資訊,請參閱 iOS/README.rst),透過執行以下命令建立 Python iOS 測試平臺專案的克隆:
$ python iOS/testbed clone --framework <path/to/Python.xcframework> --app <path/to/module1> --app <path/to/module2> app-testbed
你需要修改 iOS/testbed
引用以指向 CPython 原始碼樹中的該目錄;使用 --app
標誌指定的任何資料夾都將複製到克隆的測試平臺專案中。生成的測試平臺將在 app-testbed
資料夾中建立。在此示例中,module1
和 module2
將在執行時成為可匯入的模組。如果你的專案有其他依賴項,可以將它們安裝到 app-testbed/iOSTestbed/app_packages
資料夾中(使用 pip install --target app-testbed/iOSTestbed/app_packages
或類似命令)。
然後,您可以使用 app-testbed
資料夾來執行您的應用程式的測試套件。例如,如果 module1.tests
是您測試套件的入口點,您可以執行
$ python app-testbed run -- module1.tests
這等同於在桌面 Python 構建上執行 python -m module1.tests
。 --
之後的所有引數都將傳遞給 testbed,就像它們是桌面機器上 python -m
的引數一樣。
您還可以透過執行以下命令在 Xcode 中開啟 testbed 專案
$ open app-testbed/iOSTestbed.xcodeproj
這將允許您使用完整的 Xcode 工具套件進行除錯。
7.3. App Store 合規性¶
將應用程式分發到第三方 iOS 裝置的唯一機制是將應用程式提交到 iOS App Store;提交分發的應用程式必須透過 Apple 的應用程式稽核流程。此過程包括一組自動驗證規則,這些規則會檢查提交的應用程式包是否存在問題程式碼。
Python 標準庫包含一些已知會違反這些自動規則的程式碼。雖然這些違規行為似乎是誤報,但 Apple 的稽核規則無法質疑;因此,必須修改 Python 標準庫才能使應用程式透過 App Store 稽核。
Python 原始碼樹包含一個補丁檔案,它將刪除所有已知會導致 App Store 稽核過程出現問題的程式碼。在為 iOS 構建時,會自動應用此補丁。