PEP 665 – 一種用於列出 Python 依賴套件的檔案格式,以確保應用程式的可重現性
- 作者:
- Brett Cannon <brett at python.org>, Pradyun Gedam <pradyunsg at gmail.com>, Tzu-ping Chung <uranusjr at gmail.com>
- PEP 委託人:
- Paul Moore <p.f.moore at gmail.com>
- 討論於:
- Discourse 討論串
- 狀態:
- 已否決 (Rejected)
- 類型:
- 標準軌跡 (Standards Track)
- 主題:
- 套件封裝 (Packaging)
- 建立日期:
- 2021 年 7 月 29 日
- 公告歷史:
- 2021 年 7 月 29 日, 2021 年 11 月 3 日, 2021 年 11 月 25 日
- 被以下文件取代:
- 751
- 決議:
- Discourse 訊息
目錄
- 摘要
- 術語
- 動機
- 原理
- 規範
- 詳情
versioncreated-at (建立時間)[tool] (工具)[metadata] (中繼資料)[[package._name_._version_]] (套件._名稱_._版本_)package._name_._version_.filename (套件._名稱_._版本_.檔案名稱)[package._name_._version_.hashes] (套件._名稱_._版本_.雜湊)package._name_._version_.url (套件._名稱_._版本_.網址)package._name_._version_.direct (套件._名稱_._版本_.直接)package._name_._version_.requires-python (套件._名稱_._版本_.Python需求)package._name_._version_.requires (套件._名稱_._版本_.需求)
- 範例
- 鎖定器(Locker)的期望
- 安裝程式(Installer)的期望
- 詳情
- (潛在的) 工具支援
- 回溯相容性
- 過渡計畫
- 安全性影響
- 如何教學
- 參考實作
- 否決的想法
- 待決問題
- 致謝
- 版權
附註
此 PEP 因缺乏原始碼發行版支援而受到社群反應冷淡,故被拒絕。
摘要
此 PEP 規範了一種檔案格式,用於指定應用程式所需的 Python 套件安裝要求清單,以及所指定要求之間的關係。該要求清單被視為安裝目標的完整清單,因此除了目標平台和檔案本身之外,不需要任何其他資訊。該檔案格式足夠靈活,可以在不同平台上安裝這些要求,從而允許從同一個檔案在多個平台上實現可重現性。
術語
為了促進對此 PEP 主題的討論,必須就某些術語的定義達成共識。
「套件」(package)是您作為依賴項安裝並透過匯入系統使用的東西。PyPI 上的套件就是一個例子。
「應用程式」(application 或 app)是一種最終產品,其他外部程式碼不會透過匯入系統直接依賴它(即它們是獨立的)。桌面應用程式、命令列工具等都是應用程式的例子。
「鎖定檔案」(lock file)記錄了應用程式要安裝的套件。傳統上,鎖定檔案會指定要安裝套件的確切版本,但指定套件不一定會在給定平台上安裝(根據後續章節描述的過濾邏輯),這使得鎖定檔案能夠描述跨多個平台的可重現性。此類例子有來自 npm 的 package-lock.json,來自 Poetry 的 Poetry.lock 等。
「鎖定」(locking)是指將應用程式依賴的套件輸入,並從中產生鎖定檔案的行為。
「鎖定器」(locker)是產生鎖定檔案的工具。
「安裝程式」(installer)會消耗鎖定檔案來安裝鎖定檔案中指定的內容。
動機
應用程式需要可重現安裝的原因有幾點(我們不擔心套件開發、整合到處理 Python 應用程式外部依賴鎖定的更大系統,或其他情況下需要「彈性」而非嚴格、可重現安裝要求的情況)。
第一,可重現性簡化了開發。當您和您的開發夥伴在特定平台上都得到相同的檔案時,您就能確保大家都朝著應用程式相同的體驗方向開發。您也希望使用者安裝與您預期相同的檔案,以保證體驗與您為他們開發的相同。
第二,您希望能夠在多個平台上重現安裝的內容。由於 Python 在作業系統、CPU 等方面的可移植性,建立不受單一平台限制的應用程式非常容易且通常是可取的。因此,您需要足夠靈活,以允許平台之間套件依賴項的差異,同時在任何一個特定平台上仍保持一致性和可重現性。
第三,可重現性更安全。當您精確控制安裝了哪些檔案時,您可以確保沒有惡意行為者試圖將惡意程式碼植入您的應用程式(即某些供應鏈攻擊)。透過使用始終導致可重現安裝的鎖定檔案,我們可以完全避免某些風險。
第四,依賴 wheel 檔案格式提供了可重現性,而無需建置工具本身支援可重現性。由於 wheel 檔案是靜態的,並且在安裝過程中不執行程式碼,因此 wheel 始終會產生可重現的結果。與原始碼發行版(又稱 sdists)或原始碼樹相比,後者只有在其建置工具支援可重現性(由於內在的程式碼執行)時才能實現可重現安裝。不幸的是,絕大多數建置工具都不支援可重現建置,因此此 PEP 透過僅支援 wheel 檔案作為套件格式來緩解這個問題。
此 PEP 提出了鎖定檔案的標準,因為目前的解決方案不符合所列出的目標。目前,我們最接近鎖定檔案標準的是來自 pip 的 需求檔案格式。不幸的是,該格式本身並不能帶來可重現的安裝(它需要需求檔案和安裝程式本身都具備可選功能,這將在稍後討論)。
社群本身也顯示出對鎖定檔案的需求,因為許多工具都獨立地創建了自己的鎖定檔案格式:
不幸的是,這些工具都使用了不同的鎖定檔案格式。這意味著圍繞這些工具的工具必須是獨特的。這影響了諸如程式碼編輯器和託管服務供應商等工具,它們希望在接受使用者應用程式程式碼時盡可能靈活,但對於投入多少開發資源來增加對另一種鎖定檔案格式的支援也有其限制。標準化的格式將允許工具將工作重點放在單一目標上,並確保開發者在鎖定檔案格式之外做出的工作流程決策不會對例如託管服務供應商造成困擾。
其他程式語言社群也透過開發自己的解決方案來解決這個問題,從而證明了鎖定檔案的實用性。其中一些社群包括
過去十年來,程式語言的趨勢似乎是朝著提供鎖定檔案解決方案的方向發展。
原理
檔案格式
我們希望檔案格式在稽核鎖定檔案的變更時,能夠輕鬆地作為差異(diff)來閱讀。因此,歸功於 PEP 518 和 pyproject.toml,我們決定採用 TOML 檔案格式。
安全性優先設計
將 需求檔案格式 視為我們最接近鎖定檔案標準的格式時,該檔案格式在安全性方面存在一些問題。首先,該檔案格式根本不需要您指定套件的確切版本。這就是為什麼存在 pip-tools 等工具來幫助管理需求檔案使用者的原因。
其次,您必須透過對特定依賴項使用 --hash 引數來選擇性地指定哪些檔案可以安裝。這在 pip-tools 中也是可選的,因為它需要指定 --generate-hashes 命令列引數。這需要 pip 使用 --require-hashes 以確保沒有依賴項缺少雜湊值來進行檢查。
第三,即使您控制了可以安裝哪些檔案,它也無法阻止其他套件被安裝。如果依賴項未列在需求檔案中,pip 仍會很樂意地尋找檔案來滿足該需求。您必須將 --no-deps 指定為 pip 的引數,以防止需求檔案外部的非預期依賴項解析。
第四,該格式允許安裝 原始碼發行版檔案(又稱「sdist」)。根據其性質,安裝 sdist 需要執行任意 Python 程式碼,這意味著無法控制可能安裝哪些檔案。只有透過指定 --only-binary :all:,您才能保證 pip 為每個套件僅使用 wheel 檔案。
總結來說,為了使需求檔案達到所提議的安全性水準,使用者應始終執行以下步驟:
- 使用 pip-tools 及其命令
pip-compile --generate-hashes - 使用
pip install --require-hashes --no-deps --only-binary :all:安裝需求檔案
關鍵是,所有這些旗標,以及 pip-tools 提供的安裝內容的特異性和完整性,對於需求檔案來說都是可選的。
因此,此 PEP 中提出的提案是從設計上確保安全的,它能抵禦一些供應鏈攻擊。用於安裝的檔案雜湊值是必需的。您只能從 wheel 檔案安裝,以明確定義將放置在檔案系統中的檔案。安裝程式必須從給定平台的鎖定檔案產生確定的安裝。所有這一切都導致可重現的安裝,您可以將其視為可信賴(當您已審核鎖定檔案及其所列內容時)。
跨平台
各種已經擁有鎖定檔案的專案,例如 PDM 和 Poetry,提供了「跨平台」的鎖定檔案。這允許單一鎖定檔案在多個平台上運作,同時在每個平台上都能安裝完全相同的頂層要求,並且安裝過程保持一致/明確。
關於這為何有用,讓我們舉一個涉及 PyWeek(一個為期一週的遊戲開發競賽)的例子。假設您在 Linux 上開發,而您選擇的合作夥伴使用 macOS。現在假設評審使用 Windows。您如何確保每個人都使用相同的頂層依賴項,同時允許任何平台特定的要求(例如,某個套件在 Windows 下需要一個輔助套件)?
透過跨平台鎖定檔案,您可以確保關鍵要求在所有平台上都保持一致。然後,您還可以確保在同一平台上的所有使用者都能獲得相同的可重現安裝。
簡易安裝程式
鎖定器和安裝程式之間的關注點分離,使得安裝程式能夠執行更簡單的操作。因此,它不僅讓安裝程式更容易編寫,還有助於確保安裝程式正確建立明確、可重現的安裝。
安裝程式在建立安裝時,也可以消耗較少的計算/能源。這不僅有利於加快安裝速度,也從能源消耗的角度來看是有益的,因為安裝程式預期會比鎖定器更頻繁地執行。
這導致了一種設計,即鎖定器必須預先做更多工作,以造福安裝程式。這也意味著套件依賴項的複雜性在鎖定檔案中更簡單、更容易理解,以避免模糊性。
規範
詳情
鎖定檔案必須使用 TOML 檔案格式。由於 PEP 518 採用它用於 pyproject.toml,這不僅避免了 Python 套件生態系統中需要另一種檔案格式,還有助於使鎖定檔案更具可讀性。
鎖定檔案必須以 .pylock.toml 作為檔案名稱結尾。.toml 部分明確區分了檔案的格式,並幫助程式碼編輯器等工具適當地支援該檔案。.pylock 部分將該檔案與使用者擁有的其他 TOML 檔案區分開來,使工具更容易為 Python 鎖定檔案(而非一般 TOML 檔案)建立特定功能。
以下章節是 TOML 檔案資料格式的頂層鍵。任何未列為「必需」的欄位都被視為可選。
version
此欄位為必需。
使用的鎖定檔案版本。此鍵必須是一個字串,由遵循 核心中繼資料規範 中 Metadata-Version 鍵相同格式的數字組成。
該值必須設定為 "1.0",直到未來的 PEP 允許不同的值。引入新的可選鍵到檔案格式中應增加次要版本。引入新的必需鍵或更改格式必須增加主要版本。如何處理其他情況則留作每個 PEP 的決定。
如果鎖定檔案指定的版本其主要版本受到支援,但次要版本不受支援/無法識別(例如,安裝程式支援 "1.0",但鎖定檔案指定 "1.1"),安裝程式必須向使用者發出警告。
如果鎖定檔案指定了不受支援的主要版本(例如,安裝程式支援 "1.9" 但鎖定檔案指定 "2.0"),安裝程式必須引發錯誤。
created-at (建立時間)
此欄位為必需。
鎖定檔案產生時的時間戳記(使用 TOML 的原生時間戳記類型)。它必須使用 UTC 時區記錄以避免模糊不清。
如果設定了 SOURCE_DATE_EPOCH 環境變數,則鎖定器必須將其用作時間戳記。這有助於鎖定檔案本身的可重現性。
[tool] (工具)
工具可以在 tool 表格下建立自己的子表格。此表格的規則與 PEP 518 中 pyproject.toml 及其 [tool] 表格的規則相同,參見 建置系統宣告規範。
[metadata] (中繼資料)
此表格為必需。
包含適用於整個鎖定檔案的資料的表格。
metadata.marker (中繼資料.標記)
一個鍵,儲存一個字串,其中包含 依賴規範說明 中指定的環境標記。
鎖定器可以指定一個環境標記,該標記指定鎖定檔案生成時的任何限制。
如果安裝程式為不滿足指定環境標記的環境進行安裝,則安裝程式必須引發錯誤,因為鎖定檔案不支援目標安裝環境。
metadata.tag (中繼資料.標籤)
一個鍵,儲存一個字串,指定 平台相容性標籤(即 wheel 標籤)。此標籤可以是壓縮的標籤集。
如果安裝程式正在為不滿足指定標籤(集)的環境進行安裝,則安裝程式必須引發錯誤,因為鎖定檔案不支援目標安裝環境。
metadata.requires (中繼資料.需求)
此欄位為必需。
遵循 依賴規範說明 的字串陣列。此陣列代表鎖定檔案的頂層套件依賴項,因此也是依賴圖的根。
metadata.requires-python (中繼資料.Python需求)
一個字串,指定此鎖定檔案支援的 Python 版本。其格式與 核心中繼資料規範 中 Requires-Python 欄位指定的格式相同。
[[package._name_._version_]] (套件._名稱_._版本_)
此陣列為必需。
每個套件和版本的一個陣列,包含潛在(wheel)檔案的條目,以 _name_ 和 _version_ 分別表示。
鎖定器必須根據 簡易儲存庫 API 正規化專案名稱。如果將額外功能(extras)指定為要安裝專案的一部分,則額外功能應包含在鍵名稱中並按字典序排序。
在檔案中,專案的表格應按以下順序排序:
- 專案/鍵名稱按字典序
- 套件版本,根據 版本規範說明 從最新/最高到最舊/最低
- 可選依賴項(extras)按字典序
- 基於
filename欄位(如下所述)的檔案名稱
這些建議旨在幫助最大程度地減少工具執行之間的差異變更。
package._name_._version_.filename (套件._名稱_._版本_.檔案名稱)
此欄位為必需。
一個字串,表示檔案的基本名稱,如陣列中的條目所代表(即 os.path.basename()/pathlib.PurePath.name 所代表的)。此欄位是必需的,以簡化安裝程式,因為檔案名稱是解析從檔案名稱衍生的 wheel 標籤所必需的。它還保證了陣列條目與其所針對的檔案之間的關聯始終清晰。
[package._name_._version_.hashes] (套件._名稱_._版本_.雜湊)
此表格為必需。
一個表格,其鍵指定雜湊演算法,值為 package._name_._version_ 表格中此條目所代表檔案的雜湊值。
鎖定器應按字典序列出雜湊值。這有助於最大程度地減少差異大小以及忽略雜湊值變更的可能性。
安裝程式必須只安裝與指定雜湊值之一匹配的檔案。
package._name_._version_.url (套件._名稱_._版本_.網址)
一個字串,表示獲取檔案的 URL。
安裝程式可以支援它想要的任何 URL 方案。沒有方案的 URL 必須假定為本地檔案路徑(包括相對於鎖定檔案的相對路徑和絕對路徑)。安裝程式必須至少支援 HTTPS URL 和本地檔案路徑。
如果可以使用替代方法(例如,在檔案系統中的快取目錄中)找到匹配指定雜湊值的檔案,則安裝程式可以選擇不使用 URL 來檢索檔案。
package._name_._version_.direct (套件._名稱_._版本_.直接)
一個布林值,表示安裝程式是否應將專案視為「直接」安裝,如 已安裝發行版之直接網址來源規範 所述。
如果鍵為 true,則安裝程式必須遵循 已安裝發行版之直接網址來源規範,將安裝記錄為「直接」。
package._name_._version_.requires-python (套件._名稱_._版本_.Python需求)
一個字串,指定此檔案支援的 Python 版本。其格式與 核心中繼資料規範 中 Requires-Python 欄位指定的格式相同。
package._name_._version_.requires (套件._名稱_._版本_.需求)
遵循 依賴規範說明 的字串陣列,代表此檔案的依賴項。
範例
version = "1.0"
created-at = 2021-10-19T22:33:45.520739+00:00
[tool]
# Tool-specific table.
[metadata]
requires = ["mousebender", "coveragepy[toml]"]
marker = "sys_platform == 'linux'" # As an example for coverage.
requires-python = ">=3.7"
[[package.attrs."21.2.0"]]
filename = "attrs-21.2.0-py2.py3-none-any.whl"
hashes.sha256 = "149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"
url = "https://files.pythonhosted.org/packages/20/a9/ba6f1cd1a1517ff022b35acd6a7e4246371dfab08b8e42b829b6d07913cc/attrs-21.2.0-py2.py3-none-any.whl"
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
[[package.attrs."21.2.0"]]
# If attrs had another wheel file (e.g. that was platform-specific),
# it could be listed here.
[[package."coveragepy[toml]"."6.2.0"]]
filename = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
hashes.sha256 = "c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"
url = "https://files.pythonhosted.org/packages/da/64/468ca923e837285bd0b0a60bd9a287945d6b68e325705b66b368c07518b1/coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl"
requires-python = ">=3.6"
requires = ["tomli"]
[[package."coveragepy[toml]"."6.2.0"]]
filename = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl "
hashes.sha256 = "276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"
url = "https://files.pythonhosted.org/packages/17/d6/a29f2cccacf2315150c31d8685b4842a6e7609279939a478725219794355/coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl"
requires-python = ">=3.6"
requires = ["tomli"]
# More wheel files for `coverage` could be listed for more
# extensive support (i.e. all Linux-based wheels).
[[package.mousebender."2.0.0"]]
filename = "mousebender-2.0.0-py3-none-any.whl"
hashes.sha256 = "a6f9adfbd17bfb0e6bb5de9a27083e01dfb86ed9c3861e04143d9fd6db373f7c"
url = "https://files.pythonhosted.org/packages/f4/b3/f6fdbff6395e9b77b5619160180489410fb2f42f41272994353e7ecf5bdf/mousebender-2.0.0-py3-none-any.whl"
requires-python = ">=3.6"
requires = ["attrs", "packaging"]
[[package.packaging."20.9"]]
filename = "packaging-20.9-py2.py3-none-any.whl"
hashes.blake-256 = "3e897ea760b4daa42653ece2380531c90f64788d979110a2ab51049d92f408af"
hashes.sha256 = "67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
url = "https://files.pythonhosted.org/packages/3e/89/7ea760b4daa42653ece2380531c90f64788d979110a2ab51049d92f408af/packaging-20.9-py2.py3-none-any.whl"
requires-python = ">=3.6"
requires = ["pyparsing"]
[[package.pyparsing."2.4.7"]]
filename = "pyparsing-2.4.7-py2.py3-none-any.whl"
hashes.sha256 = "ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
url = "https://files.pythonhosted.org/packages/8a/bb/488841f56197b13700afd5658fc279a2025a39e22449b7cf29864669b15d/pyparsing-2.4.7-py2.py3-none-any.whl"
direct = true # For demonstration purposes.
requires-python = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
[[package.tomli."2.0.0"]]
filename = "tomli-2.0.0-py3-none-any.whl"
hashes.sha256 = "b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"
url = "https://files.pythonhosted.org/packages/e2/9f/5e1557a57a7282f066351086e78f87289a3446c47b2cb5b8b2f614d8fe99/tomli-2.0.0-py3-none-any.whl"
requires-python = ">=3.7"
鎖定器(Locker)的期望
鎖定器必須建立鎖定檔案,使得在指定平台上符合安裝條件的套件進行拓撲排序後,產生的圖中每個套件只剩一個版本符合安裝條件,並且每個套件至少有一個相容的檔案可供安裝。這將導致任何支援平台上的鎖定檔案,其中安裝程式唯一可以做的決定是安裝哪個「最適合的」wheel 檔案(這將在下面討論)。
鎖定器預期會適當地利用 metadata.marker、metadata.tag 和 metadata.requires-python,以及透過 requires 指定的環境標記和透過 requires-python 指定的 Python 版本要求,以確保安裝程式達到此結果。換句話說,鎖定檔案中使用的資訊預期不是來自鎖定器輸入的原始/未經處理的資訊,而是根據需要進行更改,以實現鎖定器的目標。
安裝程式(Installer)的期望
預期的解析安裝內容的演算法為:
- 以
metadata.requires作為起點/根節點,根據鎖定檔案中的資料建構依賴圖。 - 消除所有指定平台不支援的檔案。
- 根據
requires的標記評估,消除套件之間所有不相關的邊緣。 - 如果套件版本仍然可以從依賴圖的根部到達,但缺少任何相容檔案,則引發錯誤。
- 驗證所有剩餘的套件只有一個版本可以安裝,否則引發錯誤。
- 為每個剩餘套件安裝最適合的 wheel 檔案。
安裝程式必須遵循確定性演算法來決定哪個是「最適合的 wheel 檔案」。一個簡單的解決方案是依賴 packaging 專案 及其 packaging.tags 模組來確定 wheel 檔案的優先順序。
安裝程式必須支援安裝到空白環境中。安裝程式可以支援安裝到已包含已安裝套件的環境中(以及支援該功能所需的一切)。
(潛在的) 工具支援
pip 團隊 表示 如果此 PEP 獲得接受,他們有興趣支援。目前 pip 的提案甚至可能 取代 pip-tools 的需求。
Pyflow 已經表示他們 「喜歡這個 PEP 的想法」。
Poetry 表示他們不會按原樣支援此 PEP,因為 「Poetry 支援 sdists 檔案、目錄和 VCS 依賴項,這些在此 PEP 中不受支援」。在檔案層級記錄需求,其目的是為了更好地反映依賴項可能發生的情況,這 「與 Poetry 的設計相矛盾」。這也排除了將此 PEP 的鎖定檔案匯出為此 PEP 的鎖定檔案格式,因為 「Poetry 將 poetry.lock 檔案中存在的資訊匯出為另一種格式」,而 sdists 和原始碼樹都包含在 Poetry.lock 檔案中。因此,從 Poetry 的鎖定檔案到此 PEP 的鎖定檔案格式並非簡單的轉換。
回溯相容性
由於沒有關於鎖定檔案的既有規範,因此沒有明確的向下相容性問題。
至於擁有自己鎖定檔案的既有工具,將需要進行一些更新。大多數工具都記錄了鎖定檔案的名稱,但未記錄其內容。對於不將鎖定檔案提交到版本控制的專案,他們需要更新其 .gitignore 檔案的等效內容。對於將鎖定檔案提交到版本控制的專案,需要更新提交的檔案。
對於像 pipenv 這樣記錄其鎖定檔案格式的專案,他們很可能需要發布主要版本來更改鎖定檔案格式。
過渡計畫
一般而言,如果以下情況發生,此 PEP 可視為成功:
- 兩個既有工具成為鎖定器(例如 pip-tools、PDM、透過
pip freeze的 pip)。 - Pip 成為一個安裝程式。
- 一個主要的、非 Python 特定的平台支援此檔案格式(例如雲端供應商)。
這將顯示互通性、可用性以及程式設計社群/企業的接受度。
就過渡計畫而言,可能有許多步驟可以促成這個理想結果。以下是一個稍微理想化的計畫,可望讓這個 PEP 廣泛使用。
可用性
首先,可以開發一個等同於 pip freeze 的工具,它能建立一個鎖定檔案。雖然已安裝的套件本身無法提供足夠的資訊來靜態建立鎖定檔案,但使用者可以提供本地目錄和索引 URL 來建構一個。這將導致鎖定檔案比需求檔案更嚴格,因為它將鎖定檔案限制在當前平台。這也將允許人們查看他們的環境是否可重現。
其次,應該開發一個獨立的安裝程式。由於對安裝程式的要求比 pip 提供的簡單得多,因此開發一個獨立的安裝程式應該是合理的。
第三,可以開發一個工具來轉換 pip-tools 發出的鎖定版本需求檔案。就像上面概述的 pip freeze 等效工具一樣,可能需要使用者的一些輸入。但是這個工具可以作為任何擁有適當需求檔案的人的一個過渡步驟。這也可以作為一個測試,以評估 pip-tools 是否有可能增加 --lockfile 旗標來使用此 PEP。
所有這些都可能需要在 PEP 從條件性接受過渡到完全接受之前完成(並給予社群一個機會來測試此 PEP 是否潛在有用)。
互通性
此時,目標將是提高工具之間的互通性。
首先,pip 將成為一個安裝程式。透過讓最廣泛使用的安裝程式支援該格式,人們可以在鎖定器端進行創新,同時知道人們將擁有實際使用鎖定檔案所需的工具。
其次,pip 成為一個鎖定器。再次強調,pip 的影響力將使該格式迅速普及到絕大多數 Python 使用者。
第三,一個擁有既有鎖定檔案格式的專案至少支援匯出到此鎖定檔案格式(例如 PDM 或 Pyflow)。這將表明該格式符合其他專案的需求。
接受
隨著整個社群提供工具,那些不專屬於 Python 社群的工具將根據他們認為使用者想要什麼來支援檔案格式,從而展現其接受度。
首先,像程式碼編輯器這樣操作需求檔案的工具將對鎖定檔案提供同等的支援。
其次,像雲端供應商這樣的使用需求檔案的工具也將接受鎖定檔案。
此時,此 PEP 將已足夠普及,在普遍接受度方面與需求檔案不相上下,如果專案放棄自己的鎖定檔案轉而採用此 PEP,則可能超越需求檔案。
安全性影響
鎖定檔案不應引入安全性問題,而是應幫助解決這些問題。透過要求記錄檔案的雜湊值,鎖定檔案能夠幫助防止程式碼被篡改,因為雜湊詳細資訊已被記錄。僅依賴 wheel 檔案意味著可以提前知道將安裝哪些檔案並且是可重現的。鎖定檔案還有助於防止安裝可能具有惡意性的非預期套件更新。
如何教學
此 PEP 的教學將非常依賴於日常使用的鎖定器和安裝程式。然而,從概念上講,可以教導使用者,鎖定檔案指定了專案運作所需安裝的內容。應強調一致性和安全性的好處,以幫助使用者認識到他們為何應該關心鎖定檔案。
參考實作
一個概念驗證鎖定器可在 https://github.com/frostming/pep665_poc 找到。目前尚未實作安裝程式,但此 PEP 的設計表明鎖定器是實作上較困難的部分。
否決的想法
非 TOML 的檔案格式
曾短暫考慮 JSON,但由於以下原因
- TOML 已用於
pyproject.toml - TOML 更具可讀性
- TOML 產生更好的差異
因此決定採用 TOML。曾有人擔心 Python 的標準函式庫缺乏 TOML 解析器,但由於 pyproject.toml 的緣故,大多數套件工具已經在使用 TOML 解析器,因此這個問題似乎不是一個阻礙。過去也有人反駁這種擔憂,指出如果套件工具厭惡安裝依賴項並覺得他們不能捆綁套件,那麼套件生態系統還有比依賴第三方 TOML 解析器更大的問題需要糾正。
替代命名方案
曾考慮指定安裝檔案的目錄,但最終因人們不喜歡這個想法而遭到拒絕。
還曾建議不使用特殊的檔案名稱後綴,但最終決定這樣會嚴重影響工具的發現性。
支援單一鎖定檔案
曾一度考慮僅支援一個包含所有可能鎖定資訊的單一鎖定檔案。但很快就顯而易見,試圖設計一種資料格式,既能包含支援多個環境的鎖定檔案格式,又能為可重現建置提供嚴格的鎖定結果,將變得相當複雜和繁瑣。
支援鎖定檔案目錄以及單一命名為 pyproject-lock.toml 的鎖定檔案的想法也曾被考慮。但對於單一鎖定檔案情況下省略目錄的任何可能簡潔性似乎是不必要的。試圖定義適當的邏輯來決定什麼應該是 pyproject-lock.toml 檔案以及什麼應該放入 pyproject-lock.d 似乎沒有必要地複雜化。
使用平面列表而非依賴圖
此 PEP 的第一個版本提議鎖定檔案沒有依賴圖的概念。相反,鎖定檔案將精確列出特定平台應安裝的內容,這樣安裝程式就不必對安裝什麼做出任何決定,只需驗證鎖定檔案是否適用於目標平台。
這個想法最終被拒絕,因為潛在的 PEP 508 環境標記組合數量眾多。當時決定,當專案需要跨平台時,試圖讓鎖定器生成所有可能的組合作為個別鎖定檔案將會過於繁瑣。
requires 的替代名稱
requires 的其他一些名稱曾是 installs、needs 和 dependencies。最初,此 PEP 在詢問一位 Python 初學者他們更喜歡哪個術語後選擇了 needs。但根據對此 PEP 早期草案的回饋,最終選擇了 requires 這個術語。
接受 PEP 650
PEP 650 是早期嘗試解決此問題的方案,它透過指定安裝程式的 API 來解決,而不是標準化鎖定檔案格式(類似 PEP 517)。對 PEP 650 的 初步反應 可以說是溫和/不溫不火。人們似乎一直對哪些工具應該提供什麼功能來實施 PEP 感到困惑。它還可能產生更多開銷,因為它需要執行 Python API 來執行任何涉及套件的操作。
此 PEP 選擇圍繞一個「產物」而非 API 進行標準化(類似 PEP 621)。這將允許更多的工具整合,因為它消除了專門使用 Python 來建立鎖定檔案、更新它,甚至安裝鎖定檔案中列出的套件的需求。它還透過強制將依賴圖的詳細資訊寫入人類可讀的格式,從而更容易進行內省(introspection)。它還透過標準化人們需要了解的內容(例如,教學在理解其產生的產物時,在不同工具之間變得更具可移植性)來更容易分享知識。這也僅僅是其他語言社群採取的且似乎滿意的方法。
此 PEP 的接受將意味著 PEP 650 被拒絕。
按套件而非按檔案指定需求
此 PEP 的早期草案按套件層級(而非按檔案)指定依賴項。雖然這傳統上是套件系統的運作方式,但實際上它並未準確反映事物是如何被指定的。因此,此 PEP 隨後進行了更新,以反映依賴項真正可以指定的粒度。
指定鎖定器收集輸入的位置
此 PEP 未指定鎖定器如何獲取其輸入。最初的建議是部分重用 PEP 621,但由於在指定索引等方面的潛在輸入靈活性存在分歧,因此決定最好將此留給單獨的 PEP。
允許原始碼發行版和原始碼樹作為可選用支援的檔案格式
經過 廣泛討論 後,決定此 PEP 不支援原始碼發行版(亦稱 sdists)或原始碼樹作為可接受的程式碼格式。將 sdists 和原始碼樹引入此 PEP 將立即破壞可重現性和安全性目標,因為需要執行程式碼來建置 sdist 或原始碼樹。它還將大大增加(至少對)安裝程式的複雜性,因為 sdists 和原始碼樹的動態建置性質意味著安裝程式需要從建置和安裝的角度全面解決 sdists 動態產生的所有要求。
由於這一切,決定最好在接受/拒絕此 PEP之後,再單獨討論支援 sdists 和原始碼樹的問題。由於提議的檔案格式是版本化的,因此在稍後的 PEP 中引入 sdists 和原始碼樹支援是可行的。
然而,應該注意的是,此 PEP 並非阻止開發一個帶外(out-of-band)解決方案來配合此 PEP 使用。從 sdists 建置 wheel 檔案並在部署時將其與程式碼一起發布,以便它們可以包含在鎖定檔案中,這是一個選項。另一個選項是僅將需求檔案用於 sdists 和原始碼樹,然後依賴鎖定檔案來處理所有 wheel 檔案。
待決問題
無。
致謝
感謝 PDM 的 Frost Ming 和 Poetry 的 Sébastien Eustace,他們提供了關於 PEP 508 需求的動態安裝時解析的輸入。
感謝 Kushal Das 確保可重現建置仍然是此 PEP 關注的問題。
感謝 Andrea McInnes 最初解決了瑣碎爭議,並選擇了 needs 的顏色(在此之後,人們轉而支持 requires 的顏色)。
版權
本文件已進入公有領域或遵循 CC0-1.0-Universal 授權,以較寬鬆者為準。
來源: https://github.com/python/peps/blob/main/peps/pep-0665.rst
上次修改時間: 2024-07-26 12:58:25 GMT