你在 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 載入本機模組,掩蓋套件缺檔。應在暫存目錄的新虛擬環境安裝,並以隔離模式檢查實際匯入位置。