PEP 470 – 移除 PyPI 對外部託管的支援
- 作者:
- Donald Stufft <donald at stufft.io>
- BDFL-Delegate:
- Paul Moore <p.f.moore at gmail.com>
- 討論於:
- Distutils-SIG 郵件列表
- 狀態:
- 最終 (Final)
- 類型:
- 流程
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2014年5月12日
- 公告歷史:
- 2014年5月14日, 2014年6月5日, 2014年10月3日, 2014年10月13日, 2015年8月26日
- 取代:
- 438
- 決議:
- Distutils-SIG 訊息
摘要
此 PEP 提議棄用和移除 PyPI 對外部檔案託管的支援,以及棄用和移除由 PEP 438 增加的功能,特別是用於分類不同類型連結的 `rel` 資訊以及用於指示 API 版本的 meta 標籤。
原理
歷史上,PyPI 並沒有任何檔案託管方法,也沒有任何自動擷取可安裝套件的方法,它專注於提供一個中央名稱註冊表,以防止命名衝突,並作為尋找專案的發現手段。隨著時間推移,setuptools 開始爬取這些供人類閱讀的頁面,以及從這些頁面連結出去的頁面,尋找可以自動下載和安裝的東西。最終,這演變成了「簡易」API,它使用了類似的 URL 結構,但消除了任何多餘的連結和資訊,以使 API 更有效率。此外,PyPI 發展出了讓專案直接上傳發佈檔案到 PyPI 的能力,使 PyPI 除了作為索引之外,也能充當儲存庫。
這賦予了 PyPI 在 Python 生態系統中兩個同樣重要的角色:作為索引,以便於發現 Python 專案;以及作為中央儲存庫,以便於託管、下載和安裝 Python 專案。由於 PyPI 的歷史背景及其非常有機的發展,這兩個角色之間的界線模糊不清,這種模糊性給這兩個角色的終端使用者帶來了困惑,進而導致了試圖以不同身分使用 PyPI 的人們之間產生不滿,最常見的情況是當終端使用者希望將 PyPI 作為儲存庫使用,而作者卻只想將 PyPI 純粹作為索引使用時。
這種困惑的根源在於專案的終端使用者未能意識到專案是託管在 PyPI 上,還是依賴於外部服務。這通常發生在外部服務停機而 PyPI 正常運作時。人們會發現 PyPI 正常,其他專案也正常,但唯獨這個特定的專案不正常。他們常常不知道需要聯繫誰來解決這個問題,或者他們的補救步驟是什麼。
PEP 438 試圖解決這個問題,透過允許專案明確聲明它們是否使用儲存庫功能;如果沒有,它會讓安裝程式將找到的連結分類為「內部」、「可驗證的外部」或「不可驗證的外部」。PEP 438 已被接受並在 pip 1.4 中實作(於 2013 年 7 月 23 日發布),最終轉換在 pip 1.5 中實作(於 2014 年 1 月 2 日發布)。
PEP 438 成功地吸引更多人利用 PyPI 的儲存庫功能,這對於許多人來說是件好事,因為 PyPI 由全球內容傳遞網路提供支援,帶來了速度上的提升;然而,它也為終端使用者和作者都帶來了新的困惑和痛點。
透過轉向使用明確的多個儲存庫,我們可以使這兩個角色之間的界線更加明確,並消除目前處理不願將 PyPI 作為儲存庫使用者的實作所造成的「隱藏」驚喜。
關鍵使用者體驗預期
- 在系統、使用者或虛擬環境層級適當配置後,輕鬆讓外部託管「直接運作」。
- 從使用者體驗中(無論是在安裝還是發布套件時),消除所有關於令人困惑的「可驗證的外部」和「不可驗證的外部」區別的提及。
- PyPI 的儲存庫方面應該成為僅僅是預設的套件託管位置(也就是說,在大多數客戶端工具的預設配置中,它是唯一被視為「選擇退出」而非「選擇加入」的位置)。除了這一點之外,在 PyPI 上託管不應比託管自己的套件儲存庫提供更優越的使用者體驗。
- 在執行上述所有操作的同時,提供預設的行為,使其能抵禦大多數國家級敵人以下的攻擊者。
為何需要額外的儲存庫?
兩種常見的安裝工具,pip 和 easy_install/setuptools,都支援額外位置的概念,以搜尋檔案來滿足安裝要求,並且已經這樣做了多年。這意味著無需「逐步引進」新的標誌或概念,從 PyPI 以外的儲存庫安裝專案的解決方案,無論終端使用者的安裝程式有多舊(在合理範圍內),都能運作。這個概念不僅在 Python 工具中存在了一段時間,而且它是一個跨語言的概念,甚至延伸到作業系統層級,作業系統的套件工具幾乎普遍使用多儲存庫支援,這使得人們極有可能已經熟悉這個概念。
此外,多儲存庫方法是一個在狹窄範圍之外也有用的概念,不僅限於允許希望被納入 PyPI 索引部分但不想利用 PyPI 儲存庫部分的專案。這包括公司可能希望託管包含其內部套件的儲存庫,或者專案可能希望擁有多個發布「通道」,例如 alpha、beta、發布候選和最終版本。這也可以用於希望託管無法上傳到 PyPI 的檔案的專案,例如數 GB 的資料檔案,或者目前至少是 Linux Wheels。
為何不採用 PEP 438 或類似方案?
儘管額外搜尋位置支援在 pip 和 setuptools 中已經存在相當長一段時間,但對 PEP 438 的支援僅從 pip 1.4 版本才開始存在,並且在 setuptools 中仍未實作。PEP 438 的設計確實意味著即使使用較舊的安裝程式,使用者也能從不需要外部檔案的專案中受益;然而,對於確實需要外部檔案的專案,使用者仍然默默地被提供潛在不可靠,甚至更糟的是,不安全的檔案供下載。這個系統也是 Python 獨有的,因為它源於 PyPI 的歷史,這意味著這個概念幾乎可以肯定對大多數(如果不是全部)使用者來說都是陌生的,直到他們在使用 Python 工具鏈時遇到它。
此外,PEP 438 提出的分類系統在實踐中被證明對終端使用者來說極為困惑,以至於本 PEP 認為現狀是完全站不住腳的。使用此系統的使用者常見模式是嘗試安裝專案,可能會收到錯誤訊息(或者如果專案曾經上傳過東西到 PyPI 但後來切換而沒有移除舊檔案,可能不會收到錯誤訊息),看到錯誤訊息建議使用 --allow-external,他們重新發出指令並添加該標誌,很可能又收到另一個錯誤訊息,這次錯誤訊息建議也添加 --allow-unverified,然後第三次發出指令,這次終於安裝了他們想要的東西。
這種使用者體驗失敗存在有幾個原因。
- 如果 pip 可以在簡易 API 上為專案找到任何檔案,它將直接使用該檔案,而不是嘗試尋找更多檔案。這通常是正確的做法,因為嘗試尋找更多檔案會抹去 PEP 438 的大部分好處。這意味著如果一個專案曾經上傳過一個符合使用者安裝請求的檔案,無論該檔案有多舊,都將被使用。
- PEP 438 有一個隱含的假設,即大多數專案要麼會自行上傳到 PyPI,要麼會更新自身以直接連結到發布檔案。儘管最終有大量專案決定上傳到 PyPI,但其中一些這樣做僅僅是因為 PEP 438 的使用者體驗太差,以至於他們感到被迫這樣做。然而,更令人擔憂的是,很少有專案選擇直接且安全地連結到檔案,相反,它們仍然只是連結到必須經過爬取才能找到實際檔案的頁面,因此使得安全選項 (
--allow-external) 在很大程度上變得無用。 - 即使作者希望直接連結到他們的檔案,安全地這樣做也並非顯而易見。它要求在 URL 的雜湊中包含 MD5 雜湊(出於歷史原因)。如果他們不包含此雜湊,則他們的檔案將被視為「未驗證」。
- PEP 438 採取以安全為中心的觀點,不允許任何形式的未驗證專案的全局選擇加入。雖然這通常是件好事,但它會產生極其冗長和重複的指令調用,例如
$ pip install --allow-external myproject --allow-unverified myproject myproject $ pip install --allow-all-external --allow-unverified myproject myproject
多儲存庫/索引支援
安裝程式 SHOULD 實作或繼續提供將安裝程式指向多個 URL 位置的能力。使用者指示他們希望使用額外位置的確切機制,則留給每個獨立實作決定。
此外,在使用多個儲存庫時發現安裝候選套件的機制也由各個實作自行決定,然而一旦配置完成,實作不應僅因為它不是預設儲存庫而勸阻、警告或以其他方式貶低儲存庫的使用。
目前 pip 和 setuptools 都透過使用從任一儲存庫找到的最佳安裝候選套件來實作多儲存庫支援,本質上將其視為一個大型儲存庫。
安裝程式 SHOULD 也實作某種機制來移除或以其他方式禁用預設儲存庫的使用。實現這一點的確切細節由各個實作決定。
安裝程式 SHOULD 也實作某種機制,用於將使用者希望從特定儲存庫安裝的專案列入白名單和黑名單。實現這一點的確切細節由各個實作決定。
Python 套件指南 MUST 更新一個部分,詳細說明設定自己的儲存庫的選項,以便任何未來不想在 PyPI 上託管的專案可以參考該文件。這應該包括建議依賴自行託管儲存庫的專案,應在其專案描述中說明如何安裝其專案。
連結爬取的棄用與移除
PyPI 將新增一個託管模式。這個託管模式將被稱為 pypi-only,並將補充 PEP 438 已經提供給我們的三種模式:pypi-explicit、pypi-scrape、pypi-scrape-crawl。這個新的託管模式將修改專案的簡易 API 頁面,使其只列出直接託管在 PyPI 上的檔案,並且不會連結到其他任何東西。
本 PEP 獲得接受並新增 pypi-only 模式後,所有新專案將預設為 PyPI only 模式,並將鎖定在此模式,無法更改此特定設定。
隨後將向所有僅託管在 PyPI 上的專案寄送電子郵件,通知他們在一個月內,他們的專案將自動轉換為 pypi-only 模式。這些電子郵件發送一個月後,任何已收到電子郵件且仍僅託管在 PyPI 上的專案,其模式將被永久設定為 pypi-only。
同時,將向依賴 PyPI 外部託管的專案寄送電子郵件。此電子郵件將警告這些專案,PyPI 上外部託管的檔案已被棄用,並且自該電子郵件發送起三個月後,所有外部連結將從安裝程式 API 中移除。此電子郵件 MUST 包含將其專案轉換為在 PyPI 上託管的說明,並且 MUST 包含指向腳本或套件的連結,該腳本或套件將使他們能夠輸入其 PyPI 憑證和套件名稱,並自動下載並在 PyPI 上重新託管所有檔案。此電子郵件 MUST 也包含設定其自己的索引頁面的說明。此電子郵件還必須包含 PyPI 服務條款的連結,因為許多使用者可能很久以前就已註冊,可能不記得那些條款是什麼。最後,此電子郵件還必須包含一份在 PyPI 註冊的連結列表,我們能夠檢測到可安裝檔案位於這些連結上。
首次電子郵件發送兩個月後,必須向任何仍依賴外部託管的專案寄送另一封電子郵件。這封電子郵件將包含與第一封電子郵件相同的資訊,只是移除日期將是一個月後,而不是三個月後。
最後,一個月後,所有專案都將切換到 pypi-only 模式,並且 PyPI 將被修改以移除外部連結檔案功能。
變更摘要
儲存庫端
- 棄用並移除 PEP 438 所定義的託管模式。
- 限制簡易 API 僅列出儲存庫中包含的檔案。
客戶端
- 實作多儲存庫支援。
- 實作某種移除/禁用預設儲存庫的機制。
- 棄用/移除 PEP 438
影響
為了確定影響,我們檢視了所有使用類似 pip 和 setuptools 搜尋 PyPI 方法的專案,並搜尋了 PyPI 上所有可用的檔案、從 PyPI 安全連結的檔案、從 PyPI 不安全連結的檔案,以及最後在 PyPI 外部不安全可用的檔案。當在多個位置找到相同檔案時,會進行去重複,並根據以下優先順序僅在一個位置計算:PyPI > 安全地不在 PyPI 上 > 不安全地不在 PyPI 上。這給了我們最廣泛的影響定義,這意味著該專案的任何單一檔案可能不再預設可見,然而該檔案可能已存在多年,或者它可能是二進位檔案,而 PyPI 上有 sdist 可用。這表示實際影響可能會小得多,但為了避免錯誤計算,我們採用最廣泛的定義。
截至本文撰寫時,PyPI 上託管著 65,232 個專案,其中 59 個依賴於安全託管在 PyPI 外部的檔案,931 個依賴於不安全託管在 PyPI 外部的檔案。這表明 1.5% 的專案將在某種程度上受到此變更的影響,而 98.5% 的專案將繼續像往常一樣運作。此外,受影響的專案中只有 5% 使用 PEP 438 提供的功能以安全地託管在 PyPI 外部,而其中 95% 的專案則透過中間人攻擊讓其使用者面臨遠端程式碼執行的風險。
常見問題
我無法因 <X> 在 PyPI 上託管我的專案,我該怎麼辦?
首先您應該決定 <X> 是 PyPI 固有的問題,還是 PyPI 可以開發一個功能來為您解決 <X>。如果 PyPI 可以新增功能讓您在 PyPI 上託管您的專案,那麼您應該提出該功能。然而,如果 <X> 是 PyPI 固有的問題,例如希望維護對自己檔案的控制權,那麼您應該設定自己的套件儲存庫,並在您的專案描述中指示您的使用者將其添加到他們所選安裝程式將使用的儲存庫列表中。
我的使用者透過此 PEP 獲得比以往更差的體驗,我該如何解釋?
這部分答案會因每個專案而異,您需要向您的使用者解釋是什麼讓您決定在自己的儲存庫中託管,而不是使用他們安裝程式預設儲存庫列表中已有的儲存庫。然而,這部分答案也會解釋說,以前透明地包含外部連結的行為既是安全隱患(鑑於在大多數情況下,它允許中間人攻擊者在終端使用者機器上執行任意 Python 程式碼),也是可靠性問題,並且 PEP 438 試圖透過讓他們明確選擇加入來解決這個問題,但 PEP 438 也帶來了許多嚴重的可用性問題。PEP 470 代表著將模型簡化為許多使用者會熟悉的模型,這在 Linux 發行版中很常見。
轉換到儲存庫結構會破壞我的工作流程,或我的主機不允許這樣做?
有許多廉價或免費的主機,它們很樂意支援儲存庫所需的一切。特別是,您實際上不需要將檔案上傳到任何不同的地方,只要您可以生成一個具有正確結構的主機,指向您的檔案實際位置即可。這些主機中有許多使用共享域名提供免費 HTTPS,免費的 HTTPS 憑證可以從 StartSSL 獲得,或者在不久的將來從 LetsEncrypt 獲得,或者可以從許多提供商那裡廉價獲得。
你們為何不提供 <X>?
這裡的答案將取決於 <X> 是什麼,然而典型的答案通常是其中之一:
- 我們之前沒有考慮過,也沒有人提議過。
- 我們對 <X> 沒有足夠的經驗來為其妥善設計解決方案,並歡迎領域專家幫助我們提供。
- 我們是一個開源專案,目前還沒有人自願設計和實作 <X>。
額外提出新功能的 PEP 總是受到歡迎,然而它們需要有時間和專業知識的人才能準確設計 <X>。本 PEP 旨在讓我們達到一個目標,即 PyPI 的功能是直接且易於理解的基礎,類似於現有模型,例如 Linux 發行版儲存庫。
如果我反正都要運行自己的儲存庫,為何還要註冊 PyPI?
PyPI 為 Python 生態系統提供兩個關鍵功能。其中一個是作為中央儲存庫,用於儲存 pip 或其他套件管理器下載和安裝的實際檔案,而本 PEP 關注的正是此功能,如果您運行自己的儲存庫,您將取代此功能。然而,它也提供了一個中央註冊表,用於記錄誰擁有哪個名稱,以防止命名衝突,可以將其視為 Python 套件的 DNS。除了確保名稱以先到先得的方式分配之外,它還為使用者提供一個單一地點來搜尋和發現新專案。因此,簡單的答案是,您仍然應該在 PyPI 註冊您的專案,以避免命名衝突,並使人們仍然可以輕鬆發現您的專案。
已拒絕的提案
允許更容易地發現外部託管索引
本 PEP 的先前版本包含一項新增功能,該功能已添加到 PyPI 和安裝程式中,允許專案作者在 PyPI 中輸入一個 URL 列表,該列表將指示安裝程式忽略上傳到 PyPI 的任何檔案,轉而返回一個錯誤,告知終端使用者這些額外的 URL,他們可以將其添加到其安裝程式中以使安裝正常運作。
此功能已從本 PEP 的範圍中移除,因為事實證明,開發一個能夠避免類似於 PEP 438 解決方案所導致許多問題的使用者體驗問題的解決方案過於困難。如果需要,未來的 PEP 可以重新審視這個想法。
維持現有分類系統但調整選項
本 PEP 拒絕了幾項相關提案,這些提案試圖修復現有系統的一些可用性問題,但同時仍保留 PEP 438 的核心思想。
這包括
- 預設允許安全地外部託管的檔案,但禁止不安全託管的檔案。
- 預設禁止安全地外部託管的檔案,僅提供一個全域標誌來啟用它們,但禁止不安全託管的檔案。
- 繼續遵循 PEP 438 建議的路徑,移除不安全外部託管的選項,但繼續允許安全外部託管的選項。
這些提案被拒絕,因為
- PEP 438 中引入的分類系統是 PyPI 獨有的概念,即使在 Python 套件的語境中也不具通用適用性。添加額外概念會帶來成本。
- 分類系統本身難以解釋,並且預先確定專案需要哪種類型的連結需要檢查專案的
/simple/<project>/頁面,以及可能從該頁面連結的任何 URL。 - 能夠在外部託管同時仍被連結以進行自動發現,這主要是一個歷史遺留問題,帶來相當多的麻煩和複雜性,卻收穫甚微。
- 安裝程式優化或清理使用者界面的能力受到限制,因為需要進行隱式連結爬取的性質。這不僅影響到
--allow-*選項,也影響到無法判斷連結是否預期會失敗。 - 啟用選項時,此機制作用範圍非常廣泛,而 PEP 438 試圖透過每個套件的選項來限制這一點。然而,一個長期存在的專案通常會在其簡易索引中列出多個不同的 URL。其中至少有一個不再由專案控制的情況並不少見。雖然未註冊的域名在大多數情況下相對無害,但 pip 會在每個發現階段繼續嘗試從中安裝。這意味著攻擊者只需尋找依賴不安全外部 URL 的專案,並註冊過期域名來攻擊使用者。
實施此 PEP,但不要移除現有連結
這基本上是本 PEP 的向後兼容版本。它試圖讓使用舊客戶端或未實作本 PEP 的客戶端的使用者繼續運作,就像什麼都沒改變一樣。此提案被拒絕,因為這些情況中的絕大部分都是對已棄用功能的不安全使用。本 PEP 認為,默默地允許代表終端使用者執行不安全操作根本不是一個可接受的解決方案。
版權
此文件已歸入公有領域 (public domain)。
來源:https://github.com/python/peps/blob/main/peps/pep-0470.rst