PEP 427 – Wheel 二進位套件格式 1.0
- 作者:
- Daniel Holth <dholth at gmail.com>
- BDFL-Delegate:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 討論於:
- Distutils-SIG 郵件列表
- 狀態:
- 最終 (Final)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2012年9月20日
- 公告歷史:
- 2012年10月18日,2013年2月15日
- 決議:
- Python-Dev 訊息
摘要
本 PEP 描述了一種稱為「wheel」的 Python 建構套件格式。
Wheel 是一種 ZIP 格式的封存檔,具有特殊格式的檔案名稱及 .whl 副檔名。它包含單一發佈內容,該內容幾乎如同根據 PEP 376 並採用特定安裝配置進行安裝時的狀態。雖然建議使用專用的安裝程式,但 wheel 檔案也可以透過標準的「unzip」工具簡單地解壓縮至 site-packages 中,同時保留足夠的資訊,以便在日後將其內容散佈到最終的路徑。
PEP 採納情形
本 PEP 已獲採納,其定義的 wheel 版本更新為 1.0,由 Alyssa Coghlan 於 2013 年 2 月 16 日批准 [1]
原理
Python 需要一種比 sdist 更易於安裝的套件格式。Python 的 sdist 套件由 distutils 和 setuptools 建構系統定義並依賴它們,透過執行任意程式碼來進行建構、安裝與重新編譯,僅為了將其安裝到新的虛擬環境中。這種將建構與安裝混為一談的系統既緩慢又難以維護,且阻礙了建構系統與安裝程式的創新。
Wheel 試圖透過在建構系統與安裝程式之間提供更簡單的介面來解決這些問題。Wheel 二進位套件格式使安裝程式無需了解建構系統,透過將編譯時間攤銷到多次安裝中來節省時間,並消除了在目標環境中安裝建構系統的需求。
詳情
安裝 wheel 檔案「distribution-1.0-py32-none-any.whl」
Wheel 安裝在概念上分為兩個階段:
- 解壓縮 (Unpack)。
- 解析
distribution-1.0.dist-info/WHEEL。 - 檢查安裝程式是否與 Wheel-Version 相容。若小版本號較大則發出警告,若大版本號較大則中止。
- 若 Root-Is-Purelib 為「true」,則將封存檔解壓縮至 purelib (site-packages)。
- 否則將封存檔解壓縮至 platlib (site-packages)。
- 解析
- 散佈 (Spread)。
- 已解壓縮的封存檔包含
distribution-1.0.dist-info/以及(若有資料)distribution-1.0.data/。 - 將
distribution-1.0.data/的每個子目錄移至其目的地路徑。distribution-1.0.data/的每個子目錄都是目標目錄字典的一個鍵,例如distribution-1.0.data/(purelib|platlib|headers|scripts|data)。最初支援的路徑取自distutils.command.install。 - 若適用,更新以
#!python開頭的指令碼,使其指向正確的解譯器。 - 使用已安裝的路徑更新
distribution-1.0.dist-info/RECORD。 - 移除空的
distribution-1.0.data目錄。 - 將任何已安裝的 .py 編譯為 .pyc。(解除安裝程式應具備足夠的智慧,即使 RECORD 未提及,也要能移除 .pyc。)
- 已解壓縮的封存檔包含
建議的安裝程式功能
- 重寫
#!python。 - 在 wheel 中,指令碼封裝在
{distribution}-{version}.data/scripts/中。若scripts/中檔案的第一行明確以b'#!python'開頭,則將其重寫以指向正確的解譯器。如果封存檔是在 Windows 上建立的,Unix 安裝程式可能需要將這些檔案加上 +x 權限位元。允許使用
b'#!pythonw'慣例。b'#!pythonw'表示這是一個 GUI 指令碼,而非主控台指令碼。 - 產生指令碼封裝程式 (Wrappers)。
- 在 wheel 中,在 Unix 系統上封裝的指令碼肯定不會附帶 .exe 封裝程式。Windows 安裝程式可能希望在安裝期間加入這些檔案。
建議的封裝程式功能
- 將
.dist-info置於封存檔末尾。 - 鼓勵封裝程式將
.dist-info檔案實際放置在封存檔的末尾。這使得一些有趣的 ZIP 技巧成為可能,包括在不重寫整個封存檔的情況下修改元資料的能力。
檔案格式
檔案命名規範
Wheel 檔案名稱格式為 {distribution}-{version}(-{build tag})?-{python tag}-{abi tag}-{platform tag}.whl。
- distribution
- 發佈名稱,例如「django」、「pyramid」。
- version
- 發佈版本,例如 1.0。
- build tag
- 選用的建構編號。必須以數字開頭。若兩個 wheel 檔案名稱在所有其他方面(即名稱、版本及其他標籤)皆相同,則作為勝負判定依據。若未指定,則排序為空元組;否則排序為兩個項目的元組,第一項為整數形式的起始數字,第二項為標籤其餘部分的字串。
- 語言實作與版本標籤
- 例如「py27」、「py2」、「py3」。
- abi tag
- 例如「cp33m」、「abi3」、「none」。
- 平台標籤
- 例如「linux_x86_64」、「any」。
例如,distribution-1.0-1-py27-none-any.whl 是名為「distribution」套件的第一個建構版本,與 Python 2.7 相容(任何 Python 2.7 實作),無 ABI(純 Python),適用於任何 CPU 架構。
副檔名前面的最後三個檔案名稱元件稱為「相容性標籤」。相容性標籤表達了套件的基本解譯器需求,詳細說明請見 PEP 425。
跳脫字元與 Unicode
檔案名稱的每個元件皆會透過將非英數字元序列替換為底線 _ 來進行跳脫處理。
re.sub("[^\w\d.]+", "_", distribution, re.UNICODE)
封存檔名稱為 Unicode。雖然距離工具支援非 ASCII 檔案名稱還有一段時間,但本規範已予以支援。
封存檔*內部*的檔案名稱以 UTF-8 編碼。儘管某些常用的 ZIP 客戶端無法正確顯示 UTF-8 檔案名稱,但 ZIP 規範和 Python 的 zipfile 皆支援此編碼。
檔案內容
Wheel 檔案的內容(其中 {distribution} 替換為套件名稱,例如 beaglevote,{version} 替換為其版本,例如 1.0.0)包含:
/(封存檔根目錄):包含所有需依照WHEEL指定安裝至purelib或platlib的檔案。purelib和platlib通常皆為site-packages。{distribution}-{version}.dist-info/:包含元資料。{distribution}-{version}.data/:包含每個尚未涵蓋的非空安裝配置鍵的子目錄,子目錄名稱是對應安裝路徑字典的索引(例如data,scripts,headers,purelib,platlib)。- Python 指令碼必須出現在
scripts中,且必須明確以b'#!python'開頭,才能在安裝時享受自動產生的指令碼封裝程式與#!python重寫功能。它們可以有任何副檔名或沒有副檔名。 {distribution}-{version}.dist-info/METADATA:為元資料 1.1 版或更高版本的格式。{distribution}-{version}.dist-info/WHEEL:為關於封存檔本身的元資料,採用相同的「鍵:值」格式。Wheel-Version: 1.0 Generator: bdist_wheel 1.0 Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any Build: 1
Wheel-Version:Wheel 規範的版本號。Generator:產生此封存檔的軟體名稱(選用版本號)。Root-Is-Purelib:若封存檔的頂層目錄應安裝至 purelib 則為 true;否則應安裝至 platlib。Tag:Wheel 的展開相容性標籤;在上述範例中,檔案名稱將包含py2.py3-none-any。Build:建構編號;若無建構編號則省略。- 若 Wheel-Version 大於安裝程式支援的版本,安裝程式應發出警告;若大版本號大於安裝程式支援的版本,安裝程式必須失敗。
- 由於 Wheel 是一種旨在跨多個 Python 版本運作的安裝格式,因此通常不包含 .pyc 檔案。
- Wheel 不包含 setup.py 或 setup.cfg。
此版本的 Wheel 規範基於 distutils 安裝配置,並未定義如何將檔案安裝至其他位置。該佈局提供了現有 wininst 與 egg 二進位格式所提供功能的一個超集。
.dist-info 目錄
- Wheel 的 .dist-info 目錄至少包含 METADATA、WHEEL 與 RECORD。
- METADATA 是套件元資料,格式與 sdist 根目錄中的 PKG-INFO 相同。
- WHEEL 是套件建構所特有的 Wheel 元資料。
- RECORD 是 wheel 中(幾乎)所有檔案及其安全雜湊值 (Secure Hash) 的清單。與 PEP 376 不同,除無法包含自身雜湊值的 RECORD 檔案外,每個檔案都必須包含其雜湊值。雜湊演算法必須為 sha256 或更好;特別是,不允許使用 md5 與 sha1,因為已簽章的 wheel 檔案依賴 RECORD 中的強雜湊值來驗證封存檔的完整性。
- PEP 376 的 INSTALLER 與 REQUESTED 未包含在封存檔中。
- RECORD.jws 用於數位簽章。它未在 RECORD 中提及。
- 為了方便需要使用 S/MIME 簽章來保護 wheel 檔案的使用者,RECORD.p7s 是被允許的。它未在 RECORD 中提及。
- 在解壓縮期間,wheel 安裝程式會針對檔案內容驗證 RECORD 中的所有雜湊值。除了 RECORD 及其簽章外,若封存檔中的任何檔案未在 RECORD 中提及且雜湊值錯誤,安裝將會失敗。
.data 目錄
任何未正常安裝在 site-packages 內部的檔案都會放入 .data 目錄中,其命名方式為 .dist-info 目錄,但副檔名為 .data/。
distribution-1.0.dist-info/
distribution-1.0.data/
.data 目錄包含子目錄,用以存放發佈內容中的指令碼、標頭、文件等。安裝期間,這些子目錄的內容會被移至其目的地路徑。
已簽章的 wheel 檔案
Wheel 檔案包含可啟用數位簽章的擴充 RECORD。PEP 376 的 RECORD 已修改,包含安全雜湊值 digestname=urlsafe_b64encode_nopad(digest)(以 URL 安全的 base64 編碼,無結尾 = 字元),作為第二欄位以取代 md5sum。所有可能的項目皆會經過雜湊處理,包括任何產生的檔案(如 .pyc),但 RECORD 除外(因為它無法包含自身的雜湊值)。例如:
file.py,sha256=AVTFPZpEKzuHr7OvQZmhaU3LvwKz06AJw8mT\_pNh2yI,3144
distribution-1.0.dist-info/RECORD,,
簽章檔案 RECORD.jws 與 RECORD.p7s 完全沒有在 RECORD 中提及,因為它們只能在 RECORD 產生後才能加入。封存檔中的每個其他檔案必須在 RECORD 中有正確的雜湊值,否則安裝將失敗。
若使用 JSON Web Signatures (JWS),一個或多個 JSON Web Signature JSON Serialization (JWS-JS) 簽章會儲存在與 RECORD 相鄰的 RECORD.jws 檔案中。JWS 透過包含 RECORD 的 SHA-256 雜湊值作為簽章的 JSON 承載內容 (Payload) 來簽署 RECORD。
{ "hash": "sha256=ADD-r2urObZHcxBW3Cr-vDCu5RJwT4CaRTHiFmbcIYY" }
(雜湊值與 RECORD 中使用的格式相同。)
若使用 RECORD.p7s,它必須包含 RECORD 的分離式 (detached) S/MIME 格式簽章。
Wheel 安裝程式不需要了解數位簽章,但必須針對已解壓縮的檔案內容驗證 RECORD 中的雜湊值。當安裝程式檢查 RECORD 中的檔案雜湊值時,獨立的簽章檢查程式只需確認 RECORD 與簽章相符即可。
參閱:
與 .egg 的比較
- Wheel 是一種安裝格式;egg 是可匯入的。Wheel 封存檔不需要包含 .pyc,且與特定 Python 版本或實作的綁定程度較低。Wheel 可以安裝使用舊版 Python 建構的(純 Python)套件,因此您不必總是等待封裝程式跟上腳步。
- Wheel 使用 .dist-info 目錄;egg 使用 .egg-info。Wheel 與 Python 套件管理的新世界及其帶來的新概念相容。
- 對於當今多重實作的世界,Wheel 擁有更豐富的檔案命名規範。單一 wheel 封存檔可以標示其與多個 Python 語言版本與實作、ABI 及系統架構的相容性。歷史上 ABI 是特定於某個 CPython 發行版的,而 wheel 已準備好迎接穩定 ABI。
- Wheel 是無損的。第一個 wheel 實作 bdist_wheel 總是會產生 egg-info,然後將其轉換為 .whl。轉換現有的 eggs 與 bdist_wininst 發佈內容也是可能的。
- Wheel 是有版本控制的。每個 wheel 檔案都包含 Wheel 規範的版本以及封裝它的實作版本。希望下一次遷移只需升級到 Wheel 2.0。
- Wheel 是對「另一個 Python」的參考。
常見問題 (FAQ)
Wheel 定義了 .data 目錄。我應該把所有資料都放在那裡嗎?
本規範對於您應如何組織程式碼沒有立場。.data 目錄僅是存放非正常安裝於site-packages或 PYTHONPATH 中的檔案之空間。換句話說,您可以繼續使用pkgutil.get_data(package, resource),即使這些檔案通常不會在 wheel 的.data目錄中分發。
為什麼 wheel 包含附加簽章?
附加簽章比分離式簽章更方便,因為它們隨封存檔一同傳輸。由於只有個別檔案被簽署,因此可以重新壓縮封存檔而不會使簽章失效,或者可以在無需下載整個封存檔的情況下驗證個別檔案。
為什麼 wheel 允許 JWS 簽章?
作為 JWS 一部分的 JOSE 規範被設計為易於實作,這也是 Wheel 的主要設計目標之一。JWS 可提供實用、簡潔的純 Python 實作。
為什麼 wheel 也允許 S/MIME 簽章?
對於需要或想要將現有公開金鑰基礎設施 (PKI) 與 wheel 配合使用的使用者,允許使用 S/MIME 簽章。已簽章的套件僅是安全套件更新系統中的基本構建模塊。Wheel 僅提供該構建模塊。
「purelib」與「platlib」有什麼區別?
Wheel 保留了「purelib」與「platlib」的區別,這在某些平台上很重要。例如,Fedora 將純 Python 套件安裝到「/usr/lib/pythonX.Y/site-packages」,而將平台相依套件安裝到「/usr/lib64/pythonX.Y/site-packages」。一個將所有檔案置於
{name}-{version}.data/purelib且標示「Root-Is-Purelib: false」的 wheel,等同於一個將相同檔案置於根目錄且標示「Root-Is-Purelib: true」的 wheel,且同時在「purelib」與「platlib」類別中包含檔案是合法的。實際上,wheel 應僅包含「purelib」或「platlib」中的一種,具體取決於它是否為純 Python,且這些檔案應位於根目錄,並為「Root-is-purelib」設定適當的值。
是否可以直接從 wheel 檔案匯入 Python 程式碼?
從技術上講,由於結合了透過簡單解壓縮進行安裝,以及使用與zipimport相容的封存格式,部分 wheel 檔案*確實*支援直接放置在sys.path上。然而,儘管這種行為是格式設計的自然結果,但並不鼓勵實際依賴它。首先,Wheel 的設計初衷主要作為發佈格式,因此跳過安裝步驟也意味著有意避免依賴任何假定完整安裝的功能(例如能夠使用
pip和virtualenv等標準工具來捕獲與管理依賴關係,以便於審計與安全性更新追蹤,或透過將標頭檔發佈到適當位置來與 C 延伸模組的標準建構機制完全整合)。其次,雖然有些 Python 軟體被撰寫為支援直接從 zip 封存檔執行,但程式碼仍常預設已完成安裝。當透過嘗試從 zip 封存檔執行軟體而打破該假設時,失敗原因往往難以診斷(特別是在第三方庫中發生時)。造成此問題的兩個最常見原因是:CPython 不支援直接從 zip 封存檔匯入 C 延伸模組(因為任何平台上的動態載入機制均不直接支援),以及當從 zip 封存檔執行時,
__file__屬性不再參照一般的檔案系統路徑,而是參照結合了檔案系統上 zip 封存檔路徑與封存檔內模組相對路徑的組合路徑。即使軟體內部正確使用了抽象資源 API,與外部組件互動仍可能需要磁碟上有實際檔案存在。就像元類別 (metaclasses)、monkeypatching 與 metapath 匯入器一樣,如果您還不確定自己需要利用此功能,那麼您幾乎肯定不需要它。如果您仍然決定使用它,請注意,許多專案在接受錯誤報告前,會要求您在已完整安裝的套件中重現該錯誤,以確認其為真正的 bug。
參考文獻
附錄
urlsafe-base64-nopad 實作範例
# urlsafe-base64-nopad for Python 3
import base64
def urlsafe_b64encode_nopad(data):
return base64.urlsafe_b64encode(data).rstrip(b'=')
def urlsafe_b64decode_nopad(data):
pad = b'=' * (4 - (len(data) & 3))
return base64.urlsafe_b64decode(data + pad)
版權
本文檔已置於公有領域。
來源:https://github.com/python/peps/blob/main/peps/pep-0427.rst