PEP 777 – 如何重新發明輪子
- 作者:
- Emma Harper Smith <emma at python.org>
- 贊助人:
- Barry Warsaw <barry at python.org>
- PEP 委託人:
- Paul Moore <p.f.moore at gmail.com>
- 討論於:
- Discourse 討論串
- 狀態:
- 草案
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2024年10月9日
- 公告歷史:
- 2024-10-10
摘要
目前的 Wheel 1.0 規範編寫於十多年前,對於 Python 套件生態系統的變更展現了極強的穩健性。先前改善 Wheel 規範的努力 皆已被推遲,以專注於其他套件規範。然而,過去十年間 Wheel 的使用方式發生了巨大變化。多年來,對新 Wheel 功能的需求層出不窮;然而,演進 Wheel 規範的一個根本障礙在於,目前缺乏處理向後不相容功能新增的定義流程。因此,為了讓其他 PEP 能描述對 Wheel 規範的新增強功能,本 PEP 規定了未來 Wheel 修訂版的相容性要求。本 PEP 並未指定新的 Wheel 修訂版。新 Wheel 格式(「Wheel 2.0」)的規範將留待未來的 PEP 處理。
原理
目前,需要安裝程式採取新行為的 Wheel 規範變更皆屬於向後不相容,且需要提升 Wheel 元數據格式的主要版本號。至今尚未進行主要版本更新,部分原因是此類變更極具破壞性。根據 Wheel 規範,任何不支援新主要版本的安裝程式必須在安裝時中止。這意味著如果未經進一步規劃即提升主要版本,許多使用者將會遇到安裝失敗的情況,因為舊版安裝程式會拒絕接受上傳至如 Python Package Index (PyPI) 等公開套件索引的新 Wheel。仔細規劃建構工具、套件索引與套件安裝程式之間的交互作用至關重要,以避免相容性問題,特別是考慮到那些更新安裝程式速度緩慢的長尾使用者。
向後相容性的疑慮阻礙了 Wheel 檔案格式的寶貴改進,例如更好的壓縮方式、Wheel 資料格式改進、關於 Wheel 內容的更好資訊,以及 “.dist-info” 資料夾中的 JSON 格式元數據。
本 PEP 描述了新 Wheel 修訂版的限制與行為,以維持現有工具(不支援新主要版本格式的工具)的穩定性。這確保了對 Wheel 規範的向後不相容變更僅會影響已正確設定以使用新版 Wheel 的使用者與工具。透過清晰的 Wheel 規範演進路徑,未來的 PEP 將能改善 Wheel 格式,而無需重新定義全新的相容性敘事。
規範
將 Wheel-Version 元數據欄位新增至核心元數據
目前,Wheel 1.0 PEP (PEP 427) 規定 Wheel 檔案必須包含一個 WHEEL 元數據檔案,其中包含該檔案所遵循的 Wheel 規範版本。PEP 427 規定,安裝程式在安裝小版本號大於其支援範圍的 Wheel 時「必須 (MUST)」發出警告,並在安裝大版本號大於其支援範圍的 Wheel 時「必須 (MUST)」中止安裝。這確保了使用者不會因安裝程式無法正確安裝的 Wheel 而獲得無效的安裝。
然而,目前的解析器並不會排除不相容版本號的 Wheel。此外,目前沒有任何方法能在不直接下載 Wheel 的情況下檢查其版本。為了讓解析器能輕鬆進行 Wheel 版本篩選,Wheel 版本「必須 (MUST)」包含在相關的元數據檔案中(目前為 METADATA)。這將允許解析器使用 PEP 658 元數據 API 高效檢查 Wheel 版本,而無需下載並檢查 .dist-info/WHEEL 檔案。
為達成此目的,一個名為 Wheel-Version 的新欄位將被加入至 核心元數據規範 (Core Metadata Specification)。此欄位僅供單次使用,且必須包含與 WHEEL 檔案中(或未來任何定義 Wheel 檔案元數據的替代檔案中)Wheel-Version 條目完全相同的版本號。如果元數據檔案中缺少 Wheel-Version,工具「必須 (MUST)」推斷 Wheel 檔案的主要版本為 1。
Wheel-Version 「絕對不得 (MUST NOT)」包含在原始碼發行版元數據 (PKG-INFO) 檔案中。如果工具在原始碼發行版元數據檔案中遇到 Wheel-Version,它「應該 (SHOULD)」引發錯誤。
Wheel-Version 「可以 (MAY)」包含在版本 1 的 Wheel 元數據檔案中;但對於版本 2 或更高版本的 Wheel,元數據檔案「必須 (MUST)」包含 Wheel-Version。這強制要求未來 Wheel 規範的修訂版可以依靠解析器檢查 Wheel-Version 欄位來跳過不相容的 Wheel。鼓勵建構後端在所有產生的 Wheel 中包含 Wheel-Version,無論版本為何。
安裝程式「應該 (SHOULD)」在安裝過程中將 Wheel 內的元數據檔案原封不動地複製。這避免了更新 RECORD 檔案的需求,因為那是一個容易出錯的過程。讀取已安裝核心元數據的工具「不應該 (SHOULD NOT)」假定該欄位一定存在,因為其他安裝格式可能會省略它。
安裝 Wheel 時,安裝程式「必須 (MUST)」執行以下步驟:
- 檢查核心元數據檔案與 Wheel 元數據檔案中的
Wheel-Version值是否一致。如果不一致,安裝程式「必須 (MUST)」中止安裝。兩者皆無優先權。 - 檢查安裝程式是否與
Wheel-Version相容。若缺少Wheel-Version,則假設版本為 1.0。若小版本號較大則發出警告,若主要版本號較大則中止安裝。此程序與 PEP 427 中的程序完全相同。 - 依照 二進位發行版格式 (Binary Distribution Format) 規範進行安裝。
解析器對 Wheel-Version 的行為
解析器在選擇要安裝的 Wheel 時,「必須 (MUST)」檢查候選 Wheel 的 Wheel-Version,並忽略不相容的 Wheel 檔案。若不忽略這些檔案,舊版安裝程式可能會選取一個該安裝程式不支援的 Wheel 版本,導致根據 PEP 427 強制安裝程式中止。透過跳過不相容的 Wheel 檔案,當專案採用新的 Wheel 主要版本時,使用者將不會看到安裝錯誤。如 PEP 427 中已規定的,如果使用者嘗試直接安裝一個不相容的 Wheel,安裝程式「必須 (MUST)」中止。如果在解析來自多個索引的套件過程中,解析器遇到同一發行版及版本的兩個 Wheel,解析器應優先考慮相容版本號最高的 Wheel。
雖然上述措施保護了使用者免受意外中斷的影響,但如果使用者的安裝程式不支援該發行版所使用的 Wheel 版本,使用者可能會錯過新版本的發行。假設未來某個套件發佈了 3.0 的 Wheel 檔案,若下游使用者的安裝程式僅支援 2.x 的 Wheel,他們將無法察覺有新版本可用。因此,安裝程式在解析套件過程中若遇到不相容的 Wheel 並跳過它時,「應該 (SHOULD)」發出警告。
首次重大版本更新必須更改檔案副檔名
遺憾的是,現有的解析器在選取安裝候選項目之前,並不會檢查 Wheel 的相容性。在大多數使用者更新到能正確檢查 Wheel 相容性的安裝程式之前,發佈可能被現有解析器選取的新主要版本 Wheel 是不安全的。根據 PyPI 安裝程式使用情況的現有數據(詳見 附錄:PyPI 安裝程式使用分析),大多數使用者使用更新後的解析器可能需要長達四年的時間。為了進行實驗並加速 2.0 版 Wheel 的採用,本 PEP 建議將未來所有 Wheel 版本的檔案副檔名從 .whl 變更為 .whlx。請注意,whlx 中的 x 為字母「x」,並不指定 Wheel 的主要版本。副檔名名稱的變更解決了 2.0 版 Wheel 在現有未實作 Wheel-Version 檢查的安裝程式上會導致使用者安裝崩潰的初始過渡問題。透過使用不同的副檔名,2.0 版 Wheel 可以立即上傳至 PyPI,使用者也能立即體驗新功能。使用舊版安裝程式的使用者則會直接忽略這些新檔案。
其中一個被否決的替代方案是保留 .whl 副檔名,但延遲將 Wheel 2.0 發佈至 PyPI。更多資訊請參閱「被否決的構想」。
建議的新 Wheel 格式建構後端行為
建議建構後端根據專案使用的功能,產生最相容的 Wheel。例如,如果 Wheel 未使用符號連結,而該功能是在 Wheel 5.0 中引入的,則建構後端可以產生 4.0 版本的 Wheel。另一方面,某些功能預設即希望被採用,例如若 Wheel 3.0 引入了更好的壓縮,建構後端可能會希望預設啟用此功能,以改善 Wheel 大小與下載效能。
對未來 Wheel 修訂版的限制
雖然很難預測未來 Wheel 格式的規劃,但維持特定的相容性承諾至關重要。
Wheel 檔案在安裝時,「必須 (MUST)」對於所有支援的 CPython 版本皆保持與 Python 標準函式庫 importlib.metadata 的相容性。例如,將 .dist-info/METADATA 取代為 JSON 格式元數據檔案,必須是一個跨越數個主要版本的遷移過程:其中一個版本在現有 Email 標頭格式的基礎上引入新的 JSON 檔案,而未來某個版本再移除 Email 標頭格式的元數據檔案。移除 .dist-info/METADATA 的版本也「必須 (MUST)」僅在最後一個不支援該新檔案的 CPython 發行版結束生命週期 (EOL) 後才能採用。這確保了使用 importlib.metadata 的程式碼不會隨著 Wheel 主要版本的修訂而崩潰。
Wheel 檔案「必須 (MUST)」保持以 ZIP 格式作為外部容器格式。此外,.dist-info 元數據目錄「必須 (MUST)」放置在壓縮檔的根目錄且不得進行壓縮,以便解壓縮 Wheel 檔案時能產生一個包含 Wheel 元數據的標準 .dist-info 目錄。未來的 Wheel 修訂版「可以 (MAY)」修改 Wheel 非元數據組件(如資料與程式碼)的佈局、壓縮方式及其他屬性。這既確保了未來 Wheel 修訂版與操作套件元數據的工具保持相容,同時也允許改進 Wheel 中的程式碼儲存方式(如採用壓縮)。
套件工具「絕對不得 (MUST NOT)」假定 Wheel 檔案的內容與格式在未來 Wheel 主要版本中會保持不變(上述關於元數據資料夾內容與外部容器格式的限制除外)。例如,較新的 Wheel 主要版本可能會新增或移除檔案名稱組件,如建構標籤 (build tag) 或平台標籤 (platform tag)。因此,工具在嘗試安裝 Wheel 之前,有責任檢查元數據中的 Wheel-Version。
最後,未來 Wheel 修訂版「絕對不得 (MUST NOT)」使用任何非至少最新版 CPython 標準函式庫中所包含的壓縮格式。使用任何新壓縮格式產生的 Wheel 應被標記為要求至少支援該新壓縮格式的首個 CPython 發行版本,而不考慮 Wheel 內部程式碼的 Python API 相容性。
回溯相容性
對於演進 Wheel 格式而言,向後相容性是一個極其重要的議題。如果下游使用者採用新 Wheel 修訂版的過程非常痛苦,套件建立者將會猶豫是否採用新標準,而使用者則會受困於失敗的 CI 管線與其他安裝困境。
上述規範中的數個選擇旨在減輕採用新功能的痛苦。例如,目前 pip 仍會將不相容主要版本的 Wheel 選為安裝候選項目,若專案開始發佈 2.0 版本 Wheel,這會導致安裝程式失敗。為避免此問題,本 PEP 要求解析器過濾掉對安裝程式不相容的主要版本或功能的 Wheel。
本 PEP 還定義了未來 Wheel 修訂版的限制,目標是在維持與 CPython 相容性的同時,允許 Wheel 內容的演進。Wheel 修訂版不應導致在舊版 CPython 上的套件安裝失敗,因為這不僅令人沮喪,對使用者而言也將極難進行除錯。
本 PEP 依賴解析器能高效獲取套件元數據的能力,通常透過 PEP 658 達成。這對於使用不提供 PEP 658 元數據的套件索引伺服器使用者來說,可能會構成問題。然而,目前大多數安裝程式會改用 HTTP 範圍請求 (range requests) 來高效獲取讀取元數據所需的 Wheel 部分,大多數儲存提供者與伺服器皆包含此功能。此外,未來對 Wheel 的改進(如壓縮)將彌補因檢查 Wheel 內檔案而產生的效能損失。
本 PEP 的主要相容性限制在於那些同時發佈原始碼發行版與全新 Wheel 的專案。若使用舊版安裝程式的使用者嘗試安裝該套件,解析器會跳過所有較新的 Wheel,導致退回到原始碼發行版。使用者通常未妥善設定從原始碼建構專案的環境,這可能導致一些原本不會出現的建構失敗。解決此問題的方法有多種,例如在初始遷移期間允許雙重發佈,或將原始碼發行版標記為「非意圖建構」。
否決的想法
Wheel 格式已臻完美,無需更改
Wheel 格式已存在超過 10 年,期間 Python 套件發生了許多變化。套件包含 Rust 或 C 擴充模組的情況已變得非常普遍,這增加了套件的大小。更好的壓縮方式(如 lzma 或 zstd)可為 PyPI 及其使用者節省大量時間與頻寬。相容性標籤無法表達現今用於加速 Python 程式碼的各種硬體,也無法編碼共用函式庫的相容性資訊。為了解決這些問題,演進 Wheel 套件格式是必要的。
Wheel 格式的變更應與 CPython 發行版掛鉤
我不認為將 Wheel 修訂版與 CPython 發行版掛鉤是有益的。這樣做的主要好處是讓採用新 Wheel 的過程變得可預測——擁有最新 CPython 的使用者將獲得最新的套件格式!然而,此選擇存在幾個問題。首先,將新格式與最新 CPython 掛鉤會使採用速度變得非常緩慢。使用較舊 Python 安裝的 Linux LTS 版本使用者可以在虛擬環境中自由更新 pip,但無法輕鬆更新 Python 版本。雖然 Wheel 格式的某些變更必須與 CPython 變更掛鉤(例如新增壓縮格式或變更元數據格式),但許多變更並不需要與 Python 版本掛鉤,例如符號連結、增強的相容性標籤,以及使用標準函式庫中現有壓縮格式的新格式。此外,Wheel 被用於多種不同的語言實作中,這些實作往往落後於 CPython 版本。因 Python 版本而阻止其使用者使用某項功能是不公平的。最後,雖然本 PEP 不建議將 Wheel 版本與 CPython 發行版掛鉤,但未來的 PEP 可能隨時會這樣做,因此無需在本 PEP 中做出此選擇。
繼續使用 .whl 作為檔案副檔名
雖然基於多種原因保留 .whl 副檔名很具吸引力,但它帶來了幾個難以克服的問題。首先,目前的安裝程式仍會挑選一個新的 Wheel,並因無法安裝而失敗。此外,如果無法更改 Wheel 的檔案名稱,就無法避免現有預期固定 Wheel 檔案名稱格式的安裝程式崩潰。雖然目前 Wheel 的檔案名稱規範足以應付目前的使用情況,但檔案名稱中間的可選建構標籤會導致任何擴充功能變得模稜兩可(例如 foo-0.3-py3-none-any-fancy_new_tag.whl 會被解析為建構標籤為 py3)。這限制了對儲存於 Wheel 檔案名稱中資訊的變更。
將 Wheel 主要版本儲存於檔案副檔名中(如 .whl2)
將 Wheel 主要版本儲存於檔案副檔名中具有幾個不錯的優點。首先,無需引入 Wheel-Version 元數據欄位,因為安裝程式只需根據檔案副檔名進行篩選。這也將允許未來並存 (side-by-side) 放置不同版本的套件。然而,每個主要版本都更改 Wheel 的副檔名也有一些缺點。首先,儲存於 WHEEL 檔案中的版本必須與檔案副檔名相符,且這需要由安裝程式進行驗證。此外,許多系統透過副檔名關聯檔案類型(例如可執行檔關聯、各種網頁快取軟體),這些都需要隨著每個發行版本進行更新。再者,當前 Wheel 規範脆弱性的部分原因在於檔案名稱中儲存了過多的元數據。檔案名稱並不適合儲存結構化數據。未來 Wheel 修訂版的一個目標應是減少對檔案名稱中資訊編碼的依賴。
另一種可能性是使用檔案副檔名來編碼外部容器格式(即包含 .dist-info 的 ZIP 檔案),並與內部 Wheel 版本分開。然而,若副檔名與內部 Wheel-Version 出現分歧,可能會導致混亂。如果安裝程式因為從 Wheel 元數據獲得的不相容 Wheel 3.0 而引發錯誤,某些使用者會因為這與副檔名 .whl2 的差異而感到困惑。
Wheel 2.0 應更改外部容器格式
由於 Wheel 2.0 將更改 Wheel 檔案的副檔名,這正是修改外部容器格式的最佳時機。無需為了相容性而使用工具需要選擇性讀取的不同副檔名。使用不同外部壓縮格式的主要用途是更好的壓縮效能。例如,外部容器可以改為 Zstandard tarfile (.tar.zst),這將解壓縮得更快並產生更小的 Wheel。然而,這存在幾個實際問題。首先,Zstandard 並非 Python 標準函式庫的一部分,因此純 Python 套件工具需要隨附擴充模組來解壓縮這些 Wheel。這可能會導致某些擴充模組不易安裝的平台出現相容性問題。此外,未來的 Wheel 修訂版總是可以在現有的 ZIP 格式內部引入一種使用 .tar.zst 的非元數據檔案佈局。
最後,一次對 Wheel 檔案格式進行太多變更是不可取的。本 PEP 的目標是讓規範的演進變得更容易,而讓 Wheel 演進更容易背後的邏輯之一,就是避免「一次到位」的劇烈變更。更改 Wheel 的外部檔案格式不僅需要重寫套件元數據的發現方式,還需要重寫其安裝方式。
為何不在本 PEP 中指定 Wheel 2.0?
有許多功能可以作為 Wheel 2.0 的一部分,但本 PEP 未涵蓋它們。本 PEP 的目標是為 Wheel 檔案格式定義一個相容性敘事。與 Wheel 版本相容性無關的變更無需包含在本 PEP 中,並應在後續定義新 Wheel 功能的 PEP 中引入。
討論主題
索引伺服器是否應支援首次遷移的雙重發佈?
由於 .whl 與 .whlx 的檔案名稱看起來不同,它們可以同時上傳到 PyPI 等套件索引中。這有一些不錯的好處,例如對舊版與新版安裝程式的雙重支援,因此使用者既能獲得最新功能,而未升級的使用者依然能安裝套件的最新版本。
然而,這存在許多複雜之處。我們是否應允許將 Wheel 2 上傳至現有的僅支援 Wheel 1 的發行版?我們是否應對並存的 Wheel 提出任何要求,例如:
對雙重發佈 Wheel 的限制
給定的索引伺服器可能包含內容相同但 Wheel 版本不同的 Wheel,在其他因素相同的情況下,安裝程式應優先選擇最新可用的 Wheel 格式。
我們是否應該僅允許透過 PEP 694 支援「原子」雙重發佈才允許同時上傳?
致謝
本 PEP 的作者非常感謝 Barry Warsaw 與 Michael Sarahan 提供的極具價值的審查、建議與回饋。
版權
本文件已進入公有領域或遵循 CC0-1.0-Universal 授權,以較寬鬆者為準。
來源:https://github.com/python/peps/blob/main/peps/pep-0777.rst