PEP 438 – 轉向在 PyPI 上託管發行檔案
- 作者:
- Holger Krekel <holger at merlinux.eu>, Carl Meyer <carl at oddbird.net>
- BDFL-Delegate:
- Richard Jones <richard at python.org>
- 討論於:
- Distutils-SIG 郵件列表
- 狀態:
- 已取代
- 類型:
- 流程
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2013年3月15日
- 公告歷史:
- 2013年5月19日
- 被以下文件取代:
- 470
- 決議:
- Distutils-SIG 訊息
摘要
本 PEP 提出了一個向後相容的兩階段轉型流程,旨在加速、簡化並強化從 pypi.python.org (PyPI) 套件索引進行安裝的過程。為了簡化轉型並將客戶端的不便降至最低,無需對 distutils 或現有的安裝工具進行任何更改即可從第一階段轉型中獲益,這將使大多數現有套件的安裝更快速、更可靠。
第一轉型階段實作了簡單且明確的方法,讓套件維護者能控制哪些發行檔案連結會呈現給當前的安裝工具。第一階段還包括為當前套件實作分析工具,以支援與套件維護者的溝通,並自動設定用於控制發行檔案連結的預設模式。第一階段也將使 PyPI 上新註冊的專案預設為僅提供已上傳至 PyPI 的發行檔案連結。
第二轉型階段涉及終端使用者安裝工具,這些工具預設應僅安裝託管於 PyPI 上的發行檔案,並告知使用者是否存在外部發行檔案,同時提供自動使用這些外部檔案的選項。未來,外部發行檔案在註冊時必須附帶校驗雜湊值 (checksum hash),以便安裝工具能驗證最終下載檔案的完整性(託管於 PyPI 的發行檔案始終帶有此類雜湊值)。
替代性的 PyPI 伺服器實作應實作第一轉型階段中新的簡單索引服務行為,以避免安裝工具在第二階段中將其發行連結視為外部連結。
原理
外部託管的歷史與動機
當 PyPI 上線時,它提供了發行版註冊功能,但本身並未提供託管發行檔案的設施。當增加託管功能時,尚未出現自動化下載工具。當 Phillip Eby 實作自動下載(透過 setuptools)時,他選擇允許人們使用自己選擇的下載主機。尋找外部託管套件的方法實作如下:
- PyPI 的
simple/套件索引包含所有從該套件任何版本的long_description元資料中抓取到的連結。「Download-URL」與「Home-page」元資料欄位中的連結分別被賦予rel=download與rel=homepage屬性。 - 任何目標為檔案的連結,若其名稱看起來像是可安裝的原始碼或二進位發行版,且名稱格式為“packagename-version.ARCHIVEEXT”,皆會被安裝工具視為潛在的安裝候選。
- 類似地,任何後綴為“#egg=packagename-version”片段的連結皆會被視為安裝候選。
- 此外,
rel=homepage與rel=download的連結會被安裝工具進行爬取;若為 HTML,則會進一步從中抓取上述格式的發行檔案連結。
請參閱 easy_install 文件以取得此行為的完整說明。[1]
如今,PyPI 上索引的大多數套件都將發行檔案託管在 PyPI 上。在總計 29,117 個專案中,僅有 2,581 個(不到 10%)包含任何僅在 PyPI 之外可取得的可安裝檔案連結。[2]
人們選擇外部託管的原因有很多 [3],僅舉幾例:
- 發行流程與指令碼早已開發完成,且會上傳至外部網站
- 從世界某些地區上傳大檔案耗時過長
- 出口限制,例如加密相關軟體
- 公司政策要求透過自有網站提供開源套件
- 將上傳至 PyPI 整合進發行流程時遇到的問題(由於發行政策限制)
- 需要與 PyPI 維護的統計數據不同的下載數據
- 認為 PyPI 的可靠性不佳
- 不知道 PyPI 提供檔案託管服務
無論這些理由在當前是否合理,人們選擇外部託管檔案確實有其歷史背景,甚至在一段時間內這是唯一可行的方式。本 PEP 採取立場,即認為即便在今日,外部託管仍存在某些合理的原因。
問題
如今,Python 套件安裝程式(pip, easy_install, buildout 等)通常需要查詢許多非 PyPI 的網址,即使根本沒有外部託管的檔案。除了查詢 pypi.python.org 的 simple 索引頁面外,安裝程式還會爬取套件任何版本中指定的所有首頁與下載頁面。安裝程式爬取外部網站的需求減慢了安裝速度,並使安裝過程變得脆弱且不可靠。這些網站與套件也沒有參與 PEP 381 的鏡像架構,這進一步降低了全球自動化安裝過程的可靠性與速度。
大多數套件直接託管在 pypi.python.org 上 [2]。即使對於這些套件,若指定了首頁與下載網址,安裝程式仍會對其進行爬取。許多套件上傳者並不知道在套件元資料中指定「homepage」或「download-url」會不必要地減慢所有使用者的安裝過程。
依賴第三方網站也為使用自動安裝的網站帶來了更多注入惡意套件的攻擊向量。一種簡單的攻擊可能僅涉及取得一個過期不再使用的首頁網域,並將惡意套件置於該處。此外,在安裝網站與任何下載網站之間執行中間人 (MITM) 攻擊,可以在安裝網站上注入惡意套件。由於許多首頁與下載位置使用 HTTP 而非 HTTPS,這類攻擊並不難發起。即便對於從未打算在外部託管檔案的套件,由於其首頁會被安裝程式存取,這類中間人攻擊也可能輕易發生。
目前除了刪除所有歷史版本中所有的首頁/下載網址元資料外,套件維護者沒有其他方法可以避免外部連結爬取。雖然已經有人編寫了一個指令碼 [4] 來執行此操作,但這並非一個好的通用解決方案,因為它會移除 PyPI 發行版中實用的元資料。
即使「Homepage」與「Download-URL」連結所參考的網站不再被抓取連結,在目前的系統下,套件擁有者也沒有明顯的方法可以從 long_description 元資料欄位(顯示在 /pypi/PKG 作為套件文件)連結到可安裝檔案,而不讓安裝工具自動將該檔案視為安裝候選。反之,若不放入元資料欄位,也沒有辦法明確註冊多個外部發行檔案。
目標
透過實作本 PEP,旨在達到以下目標:
- 套件擁有者應能明確控制 PyPI 向安裝工具呈現哪些檔案作為安裝候選。不應因廣泛且不必要的連結爬取而減慢安裝速度並降低可靠性,特別是那些套件擁有者並未明確指定為安裝檔案的連結。
- 套件擁有者應能繼續選擇將發行檔案託管在自己的伺服器上,位於 PyPI 之外。使用者應能輕易使用自動化安裝工具要求安裝此類發行版,特別是在外部發行檔案註冊時已附帶校驗雜湊值的情況下。
- 自動化安裝工具預設不應安裝外部託管的套件,而需要使用者明確授權。當工具預設拒絕安裝此類套件時,應明確告知使用者安裝程式需要存取的外部連結,以及使用者可以提供哪些選項來授權工具存取這些連結。PyPI 應提供所有必要的元資料,以便安裝工具能輕鬆地在單次請求/回應交互中實作此功能。
- 從現狀到上述目標的遷移應是漸進式的,並將破壞降至最低。這包括提供工具,讓現有發行流程會上傳至非 PyPI 託管服務的套件擁有者,也能輕鬆將這些發行檔案上傳至 PyPI。
解決方案 / 兩個轉型階段
第一轉型階段為 PyPI 上的每個專案引入了「託管模式 (hosting-mode)」欄位,允許套件擁有者明確控制在機器可讀的 simple/ 索引中,哪些發行檔案連結會呈現給當前的安裝工具。在個別早期採用者成功操作託管模式後,第一階段將根據自動分析為現有套件設定預設託管模式。維護者將在任何此類自動變更前一個月收到通知。在第一轉型階段完成時,預計所有當前現有的發行與安裝流程及工具都將繼續運作。任何剩餘的錯誤或問題預計僅與個別套件的安裝有關,且若維護者無法聯繫,套件維護者或 PyPI 管理員可以輕鬆修正。
同樣在第一階段,simple/ 索引中提供的每個連結,若是由索引本身託管的,將被明確標記為 rel="internal"(即使是在獨立的網域上,若索引使用 CDN 來提供檔案服務)。任何未標記此屬性的連結將被視為外部連結。
在第二轉型階段,PyPI 客戶端安裝工具將進行更新,預設僅安裝 rel="internal" 的套件,除非使用者指定了允許從外部連結安裝的選項。關於安裝工具應如何運作的詳細資訊,請參閱 第二轉型階段。
目前在非 PyPI 網站上託管發行檔案的套件維護者,將收到相關說明與工具,以協助其歷史與未來的套件發行檔案進行「重新託管 (re-hosting)」。此重新託管工具必須在向套件維護者宣布自動託管模式變更之前提供。
實作
託管模式
第一轉型階段的基礎是為套件引入三種 PyPI 託管「模式」,這會影響 simple/ 索引產生的連結。這些模式透過改變產生機器可讀 simple/ 索引的演算法來實作,無需對安裝工具進行變更。
這些模式為:
pypi-scrape-crawl:如 歷史 所述,這與目前為安裝工具產生機器可讀連結的情況沒有任何變更。pypi-scrape:對於處於此模式的套件,要加入simple/索引的連結仍會從套件元資料中抓取。然而,「Home-page」與「Download-url」連結會被賦予rel=ext-homepage與rel=ext-download屬性,而非rel=homepage與rel=download。此操作的效果(無需對安裝工具進行任何更改)是,當前的安裝工具將不會追蹤這些連結以抓取更多候選連結:僅有直接託管於 PyPI 或直接從 PyPI 元資料連結的可安裝檔案會被考慮安裝。安裝工具可以發展出提供選項來使用新的 rel 屬性來爬取外部頁面,但決不能預設開啟。pypi-explicit:對於處於此模式的套件,僅有已上傳至 PyPI 的發行檔案連結,以及套件擁有者明確指定要加入simple/索引的外部發行檔案連結,才會被加入。PyPI 將為套件擁有者提供一個新介面來提供外部發行檔案 URL。這些 URL 必須包含格式為“#hashtype=hashvalue”的 URL 片段,以指定外部連結檔案的雜湊值,安裝工具必須使用該值來驗證其下載的檔案是否正確。
因此,希望最終 PyPI 上的所有專案都能遷移至 pypi-explicit 模式,同時保持透過安裝工具安裝外部託管發行檔案的能力。本 PEP 不規範廢棄其他託管模式以最終僅允許 pypi-explicit 模式,但預計在成功實作本 PEP 所述的轉型階段後的一段時間內,這將變得可行。預計廢棄流程需要一套處理被遺棄套件的新機制,以應對仍然受歡迎但維護者無法聯繫的專案。
第一轉型階段(PyPI)
所提出的解決方案包含多個實作與溝通步驟:
- 在 PyPI 中實作上述三種模式,並提供介面讓套件擁有者為每個套件選擇模式並註冊明確的外部檔案 URL。
- 對於所有模式的套件,在
simple/索引中將索引託管的檔案連結標記為rel="internal",以便客戶端工具在第二階段更容易區分這些連結。 - 在所有
simple/索引頁面加入 HTML 標籤<meta name="api-version" value="2">,讓客戶端能夠區分提供rel="internal"元資料的索引與不提供的舊版索引。 - 將所有新註冊的套件預設為
pypi-explicit模式(套件擁有者仍可視需要切換至其他模式)。 - 確定(透過自動分析 [2])哪些套件的所有可安裝檔案皆可在 PyPI 上取得(組 A),哪些所有可安裝檔案皆在 PyPI 或直接從 PyPI 元資料連結(組 B),以及哪些有可安裝版本僅從外部首頁/下載 HTML 頁面連結(組 C)。
- 向組 A 專案的維護者發送郵件,告知其專案將在一個月內自動配置為
pypi-explicit模式;同樣地,向組 B 專案的維護者告知其專案將自動配置為pypi-scrape模式。告知他們預計此變更完全不會影響其專案的可安裝性,但會為其使用者帶來更快、更安全的安裝體驗。鼓勵他們儘早親自設定此模式以造福使用者。 - 向組 C 套件的維護者發送郵件,告知其套件託管模式為
pypi-scrape-crawl,列出目前被爬取的網址,並建議他們將套件直接重新託管至 PyPI 並切換至pypi-explicit,或至少在 PyPI 元資料中提供指向發行檔案的直接連結並切換至pypi-scrape。提供相關說明與工具以協助這些轉型。
第二轉型階段(安裝工具)
對於第二轉型階段,安裝工具的維護者受邀發布兩次更新。
第一次更新應提供明確的警告,告知若選擇了外部託管的發行檔案(即其連結不包含 rel="internal" 的檔案),並明確說明這是針對哪些專案及網址,並警告未來的版本將預設停用外部託管的下載。
第二次更新應變更預設模式,僅允許安裝 rel="internal" 的套件檔案,且僅在使用者的明確選項下允許安裝外部託管的套件。
安裝程式應區分可驗證與不可驗證的外部連結。可驗證的外部連結是指從 PyPI simple/ 索引到可安裝檔案的直接連結,其 URL 片段中包含雜湊值(“#hashtype=hashvalue”),可用於驗證已下載檔案的完整性。不可驗證的外部連結是指任何不帶雜湊值(除安裝工具使用者明確提供者外)、從外部 HTML 抓取或透過其他非 PyPI 來源(例如 setuptools 的 dependency_links 功能)注入搜尋結果中的連結。
安裝程式應提供一項全面性選項,允許安裝任何可驗證的外部連結。不可驗證的外部連結僅應在使用者提供的選項明確指定可以使用哪些外部網域,或針對哪些特定套件名稱可以使用外部連結的情況下才能安裝。
當預設配置不允許下載外部託管的套件時,應通知使用者,說明如何使安裝成功,並警告其含義(檔案將從不屬於套件索引的網站下載)。針對不可驗證連結給出的警告應明確指出安裝程式無法驗證下載檔案的完整性。針對可驗證外部連結給出的警告應僅指出檔案將從外部 URL 下載,但檔案完整性可透過校驗和驗證。
替代性的 PyPI 相容索引實作應儘快升級,開始提供 rel="internal" 元資料與 <meta name="api-version" value="2"> 標籤。對於尚未在其 simple/ 頁面中提供 meta 標籤的替代索引,安裝工具應提供向後相容的後備行為(將連結視為內部連結,如 PEP 實作前,並提供警告)。
提交外部發行網址的 API
新的發行版網址可透過對該 URL 執行 HTTP POST 提交:
使用以下表單編碼資料:
| Name (名稱) | 值 |
| :action | 字串 “urls” |
| 名稱 | 套件名稱(字串) |
| version | 發行版本(字串) |
| new-url | 要儲存的新 URL |
| submit_new_url | 字串 “yes” |
POST 請求必須伴隨 HTTP Basic Auth 標頭,其中編碼了在 PyPI 上有權維護該套件的使用者之使用者名稱與密碼。
此請求的 HTTP 回應將會是以下之一:
| 代碼 | 意義 | URL 提交結果 |
| 200 | OK | 一切運作正常 |
| 400 | Bad request | 提交提供的資料格式錯誤 |
| 401 | Unauthorised | 提供的使用者名稱或密碼不正確 |
| 403 | Forbidden | 使用者無權更新套件資訊(非擁有者或維護者) |
參考文獻
致謝
感謝 Phillip Eby 提供準確的資訊,以及透過僅變更伺服器端來實作轉型的基本構想。
感謝 Donald Stufft 推動遠離外部託管,並提議為必要的 PyPI 變更實作 Pull Request 以及驅動第一階段轉型的分析工具。
感謝 Marc-Andre Lemburg, Alyssa Coghlan 以及 catalog-sig 總體對於深入思考擺脫「外部託管」相關問題的貢獻。
版權
此文件已歸入公有領域 (public domain)。
來源: https://github.com/python/peps/blob/main/peps/pep-0438.rst
最後修改: 2025-02-01 08:59:27 GMT