雲端 Mac 的 Python wheel 發布驗收:標籤與原生程式庫

雲端 Mac 的 Python wheel 發布驗收:標籤與原生程式庫

你在 Windows 上修改 Python 專案,將含有原生擴充模組的套件交給雲端 Mac 建置,日誌也顯示 wheel 已成功產生;同事安裝後,卻遇到架構不符或找不到動態程式庫的錯誤。問題通常不在於能否產生壓縮檔,而在於套件裡包含了什麼、相依項目指向哪裡,以及測試時是否不知不覺用到了原始碼目錄。

以下從含有 pyproject.toml 的既有專案出發,建立標籤檢查、原生程式庫檢查與隔離安裝三道驗收程序。不涉及上傳套件儲存庫,也不將本機測試通過視為相容於所有系統。

一、先固定直譯器與驗收範圍

在遠端 Mac 的專案根目錄執行以下命令。請使用已安裝且符合專案需求的 Python,不要在驗收期間順手升級直譯器。

python3 -m venv .wheel-build
.wheel-build/bin/python -m pip --version
.wheel-build/bin/python -c \
'import platform, sys; print(sys.version); print(platform.machine()); print(sys.executable)'
sw_vers
xcodebuild -version

將輸出與提交識別碼一併保存。直譯器版本、處理程序架構與系統版本,都是驗收的前提條件;機器採用 Apple Silicon,不代表目前的 Python 處理程序一定以 arm64 執行。

先明確定義要支援的 Python 版本、最低 macOS 版本,以及是否包含原生擴充模組。純 Python 套件也可能依賴原生套件,不能只檢查頂層專案。

wheel 檔名中的平台標籤是相容性宣告,不是轉換工具。將 arm64 檔案改名為 universal2,並不會讓二進位檔多出另一種架構。

二、從一次目標明確的建置開始

先確認 dist 中沒有上次留下的套件,再產生本次要驗收的檔案。建置後端及其版本應透過專案採用的約束機制固定;預設的建置隔離不會自動鎖定所有建置相依項目的版本。

mkdir -p dist
find dist -maxdepth 1 -name '*.whl' -print
.wheel-build/bin/python -m pip wheel \
  --no-deps --wheel-dir dist .

如果前面的檢查列出舊檔案,請先將它們移到獨立的封存目錄,不要混在一起驗收。--no-deps 只表示不為執行階段相依項目產生 wheel,並不禁止在建置階段下載相依項目。

以下檢查要求目錄內恰好只有一個 wheel,並輸出內部中繼資料:

.wheel-build/bin/python - <<'PY'
from pathlib import Path
from zipfile import ZipFile

wheels = list(Path("dist").glob("*.whl"))
if len(wheels) != 1:
    raise SystemExit("Expected exactly one wheel")
wheel = wheels[0]
print(wheel.name)
with ZipFile(wheel) as archive:
    metadata = [
        name for name in archive.namelist()
        if name.endswith(".dist-info/WHEEL")
    ]
    if len(metadata) != 1:
        raise SystemExit("Invalid wheel metadata")
    print(archive.read(metadata[0]).decode("utf-8"))
PY

核對 Tag 與檔名中的宣告是否符合交付目標。py3-none-any 通常用於與平台無關的套件;若套件內藏有原生執行檔,就需要重新檢視封裝方式,而不是直接相信標籤。

三、追查原生程式庫是否依賴建置主機

將 wheel 解壓縮到暫存目錄,檢查擴充模組與動態程式庫。以下程式碼僅供檢查,不會執行套件內的程式:

inspect_dir="$(mktemp -d)"
.wheel-build/bin/python - "$inspect_dir" <<'PY'
from pathlib import Path
from zipfile import ZipFile
import sys

wheel, = Path("dist").glob("*.whl")
with ZipFile(wheel) as archive:
    archive.extractall(sys.argv[1])
PY
find "$inspect_dir" -type f \
  \( -name '*.so' -o -name '*.dylib' \) \
  -exec file {} \; -exec otool -L {} \;

這段解壓縮流程應用於自己專案中可信任的產物;不要將來源不明的壓縮檔直接解壓縮到工作目錄。

重點是找出使用者家目錄、建置工作區與本機套件管理工具的路徑前綴。這些絕對路徑在另一台機器上往往不存在。系統程式庫路徑不能一概視為錯誤;@rpath、@loader_path 也不代表一定正確,仍須檢查實際封裝的程式庫及其載入路徑設定。

修改後必須重新建置,再次驗收。不要只修補解壓縮目錄中的檔案,卻交付未經修改的原始 wheel。檢查完成後,刪除本次建立的暫存目錄。

四、離開原始碼目錄,實際安裝測試

先將 wheel 與全部執行階段相依項目準備到獨立目錄。這個步驟可能需要連線至網路;相依項目的版本應由專案的發布約束控制,並保存最終下載清單。

mkdir -p wheelhouse
.wheel-build/bin/python -m pip download \
  --only-binary=:all: --dest wheelhouse dist/*.whl

若某個相依項目沒有符合條件的二進位套件,這一步就應該失敗。不要悄悄改成從原始碼建置,否則驗收目標就已經改變。

接著在原始碼目錄之外建立環境;執行時請輸入專案實際使用的匯入名稱,它可能與發行套件名稱不同:

read -r -p "Import module: " module
wheelhouse="$(pwd)/wheelhouse"
wheel="$(find "$(pwd)/dist" -maxdepth 1 -name '*.whl' -print)"
sandbox="$(mktemp -d)"
python3 -m venv "$sandbox/venv"
(
  cd "$sandbox"
  ./venv/bin/python -m pip install \
    --no-index --find-links "$wheelhouse" "$wheel"
  ./venv/bin/python -m pip check
  ./venv/bin/python -I - "$module" <<'PY'
import importlib
import sys

module = importlib.import_module(sys.argv[1])
print(getattr(module, "__file__", None))
print(list(getattr(module, "__path__", [])))
PY
)

確認匯入位置位於新環境內,而不是原始碼樹。pip check 只會檢查宣告的相依關係,不會驗證原生程式庫能否載入,更不會呼叫延遲載入的功能。因此,還要執行一次真正用到擴充模組的業務操作,例如處理一份固定的小型樣本,並以斷言檢查結果。

五、將通過條件寫入發布紀錄

每次至少保留提交識別碼、直譯器資訊、wheel 檔名、雜湊值、相依項目清單與業務冒煙測試結果。使用 shasum -a 256 dist/*.whl 記錄雜湊值,確保交付的正是剛才測試過的同一個檔案。

檢查失敗時,依問題層次處理:標籤不符就回頭檢查建置目標,缺少動態程式庫就檢查相依程式庫的收集流程,只有在原始碼目錄內才能匯入則檢查套件探索規則與資源宣告。不要靠擴大搜尋路徑來掩蓋檔案缺漏。

最後,在宣告支援的最低系統版本及其他目標直譯器上,重複安裝與執行測試。在較新系統上成功一次,只能證明目前的組合通過;清楚記錄驗證範圍,比替套件貼上更寬泛的標籤可靠。

常見問題

在 Apple Silicon 建置成功,就能標示為 universal2 嗎?

不能。相關原生二進位檔必須同時包含 arm64 與 x86_64,且要分別驗證執行行為;重新命名 wheel 不會增加相容性。

為什麼匯入測試必須離開原始碼目錄?

原始碼目錄可能讓 Python 載入本機模組,掩蓋套件缺檔。應在暫存目錄的新虛擬環境安裝,並以隔離模式檢查實際匯入位置。

把工作交給專屬雲端 Mac

租用一台專屬實體 Mac mini,依照工作安排選擇節點與計費週期。開通後可在控制台查看連線資訊;使用期間若遇到問題,也能在控制台提交工單。

選擇雲端 Mac 方案

依照工作安排選擇節點

確認好規格與計費週期,再開通專屬實體 Mac mini。