PEP 438 – 转向 PyPI 上的发布文件托管
- 作者:
- Holger Krekel <holger at merlinux.eu>, Carl Meyer <carl at oddbird.net>
- BDFL 委托:
- Richard Jones <richard at python.org>
- 讨论至:
- Distutils-SIG 邮件列表
- 状态:
- 已取代
- 类型:
- 流程
- 主题:
- 打包
- 创建日期:
- 2013年3月15日
- 发布历史:
- 2013年5月19日
- 取代者:
- 470
- 决议:
- Distutils-SIG 消息
摘要
本 PEP 提出了一个向后兼容的两阶段过渡过程,以加快、简化和增强从 pypi.python.org (PyPI) 软件包索引进行安装的健壮性。为了简化过渡并最大限度地减少客户端摩擦,**无需更改 distutils 或现有安装工具即可从第一阶段过渡中受益,这将为大多数现有软件包带来更快、更可靠的安装**。
第一阶段过渡实现了简单明确的方法,供软件包维护者控制哪些发布文件链接提供给当前的安装工具。第一阶段还包括对当前软件包的分析工具的实现,以支持与软件包维护者的沟通以及发布文件链接控制的默认模式的自动设置。第一阶段还将默认新注册的 PyPI 项目仅提供上传到 PyPI 的发布文件链接。
第二阶段过渡涉及最终用户安装工具,它将默认仅安装托管在 PyPI 上的发布文件,并告知用户是否存在外部发布文件,提供自动使用这些外部文件的选择。未来,外部发布文件将与校验和哈希一起注册,以便安装工具可以验证最终下载的完整性(PyPI 托管的发布文件始终带有此类校验和)。
替代的 PyPI 服务器实现应实现过渡阶段 1 的新简单索引服务行为,以避免安装工具在阶段 2 中将其发布链接视为外部链接。
基本原理
外部托管的历史和动机
当 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 上。在 PyPI 上的 29,117 个项目中,只有 2,581 个(不到 10%)包含任何仅在 PyPI 外部可用的可安装文件链接。[2]
人们选择外部托管有许多原因[3]。仅举几例:
- 发布流程和脚本已经开发并上传到外部站点
- 从世界上某些地方上传大文件太慢
- 出口限制,例如针对加密相关软件
- 公司政策要求通过自有站点提供开源软件包
- 将上传到 PyPI 整合到发布流程中的问题(由于发布政策)
- 希望获得与 PyPI 维护的下载统计数据不同的下载统计数据
- PyPI 被认为可靠性差
- 不知道 PyPI 提供文件托管服务
无论这些原因在今天是否仍然有效,显然人们选择外部托管有其历史原因,甚至在一段时间内,这是唯一可行的方式。本 PEP 认为,即使在今天,外部托管仍然有一些有效的原因。
问题
**如今,Python 软件包安装程序(pip、easy_install、buildout 等)即使没有外部托管文件,也经常需要查询许多非 PyPI URL**。除了查询 pypi.python.org 的简单索引页面外,还会抓取软件包任何版本中指定的所有主页和下载页面。安装程序需要抓取外部站点会减慢安装速度,并导致安装过程脆弱且不可靠。这些站点和软件包也不参与 PEP 381 镜像基础设施,进一步降低了全球自动化安装过程的可靠性和速度。
大多数软件包直接托管在 pypi.python.org 上 [2]。即使对于这些软件包,如果指定了主页和下载 URL,安装程序仍然会抓取它们。许多软件包上传者不知道在他们的软件包元数据中指定“homepage”或“download-url”会不必要地减慢所有用户的安装过程。
依赖第三方站点也为向使用自动化安装的站点注入恶意软件包打开了更多的攻击向量。一个简单的攻击可能只是获取一个旧的现在不使用的主页域名,并在那里放置恶意软件包。此外,在安装站点和任何下载站点之间执行中间人 (MITM) 攻击可以向安装站点注入恶意软件包。由于许多主页和下载位置使用 HTTP 而不是 HTTPS,因此此类攻击不难发起。即使对于从不打算外部托管文件的软件包,此类 MITM 攻击也可能轻易发生,因为安装程序无论如何都会联系它们的主页。
目前,软件包维护者无法避免外部链接抓取,除非删除所有历史版本的所有主页/下载 URL 元数据。虽然已经编写了一个脚本[4]来执行此操作,但这不是一个好的通用解决方案,因为它会从 PyPI 发布中删除有用的元数据。
即使“Homepage”和“Download-URL”链接引用的站点没有被抓取以获取更多链接,在当前系统下,软件包所有者也无法从 long_description 元数据字段(在 /pypi/PKG 上显示为软件包文档)链接到可安装文件,而安装工具不会自动将该文件视为安装候选。反之,如果没有将多个外部发布文件放入元数据字段,也无法显式注册它们。
目标
这些是通过实施本 PEP 旨在实现的目标
- 软件包所有者应该能够明确控制 PyPI 向安装工具呈现哪些文件作为安装候选。安装不应因软件包所有者未明确指定为安装文件的链接的广泛和不必要的抓取而变慢和降低可靠性。
- 软件包所有者应始终能够选择将其发布文件托管在自己的 PyPI 外部托管上。用户应该能够通过自动化安装工具轻松请求安装此类发布,尤其是当外部发布文件与校验和哈希一起注册时。
- 自动化安装工具**默认情况下**不应安装外部托管的软件包,但需要用户明确授权才能这样做。当工具默认拒绝安装此类软件包时,它们应该准确地告诉用户安装程序需要遵循哪些外部链接,以及用户可以提供哪些选项来授权工具遵循这些链接。PyPI 应该提供所有必要的元数据,以便安装工具能够轻松地在单个请求/回复交互中实现这一点。
- 从现状到上述目标的迁移应该是渐进的,并最大限度地减少破坏。这包括使软件包所有者更容易使用现有发布流程(上传到非 PyPI 托管)的工具,以便同时将这些发布文件上传到 PyPI。
解决方案 / 两个过渡阶段
第一阶段过渡引入了 PyPI 上每个项目的“托管模式”字段,允许软件包所有者明确控制在机器可读的 simple/ 索引中向当前安装工具提供哪些发布文件链接。在单个早期采用者成功操作托管模式后,第一阶段过渡将根据自动分析为现有软件包设置默认托管模式。**在任何此类自动更改前一个月,维护者将收到通知**。在第一阶段过渡完成时,**所有当前的现有发布和安装过程和工具预计将继续工作**。任何剩余的错误或问题预计仅与单个软件包的安装有关,并且如果维护者无法联系,则可以由软件包维护者或 PyPI 管理员轻松纠正。
同样在第一阶段,simple/ 索引中提供的每个链接,如果由索引本身托管(即使在单独的域名上,如果索引使用 CDN 提供文件服务,则可能如此),将明确标记为 rel="internal"。任何未如此标记的链接将被视为外部链接。
在第二阶段过渡中,PyPI 客户端安装工具将被更新,默认只安装 rel="internal" 软件包,除非用户指定选项允许从外部链接安装。有关安装程序应如何行为的详细信息,请参阅第二阶段过渡。
当前在非 PyPI 站点托管发布文件的软件包维护者将收到说明和工具,以方便“重新托管”其历史和未来的软件包发布文件。此重新托管工具必须在向软件包维护者宣布自动托管模式更改之前可用。
实施
托管模式
第一阶段过渡的基础是引入软件包的 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-attribution 来抓取外部页面,但默认情况下不得使用此选项。pypi-explicit:对于处于此模式的软件包,只有上传到 PyPI 的发布文件链接以及软件包所有者明确指定的外部发布文件链接才会被添加到simple/索引中。PyPI 将提供一个新的接口,供软件包所有者提供外部发布文件 URL。这些 URL 必须包含一个 URL 片段,形式为“#hashtype=hashvalue”,指定外部链接文件的哈希值,安装工具必须使用该哈希值来验证他们下载的是预期文件。
因此,希望最终 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,列出当前被抓取的 URL,并建议他们要么将其软件包直接重新托管到 PyPI 并切换到pypi-explicit,要么至少在 PyPI 元数据中提供发布文件的直接链接并切换到pypi-scrape。提供说明和工具以帮助进行这些过渡。
第二个过渡阶段 (安装工具)
对于第二阶段过渡,要求安装工具的维护者发布两次更新。
第一次更新应提供明确警告,如果选择下载外部托管的发布文件(即链接不包含 rel="internal" 的文件),应明确说明哪些项目和 URL 发生了这种情况,并警告未来版本将默认禁用外部托管下载。
第二次更新应更改默认模式,仅允许安装 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 之前一样将链接视为内部链接并提供警告)。
提交外部分发 URL 的 API
可以通过向以下 URL 执行 HTTP POST 请求来提交新的分发 URL:
并附带以下表单编码数据
| 名称 | 值 |
| :action | 字符串“urls” |
| 名称 | 软件包名称,作为字符串 |
| version | 发布版本,作为字符串 |
| new-url | 要存储的新 URL |
| submit_new_url | 字符串“yes” |
POST 请求必须附带 HTTP 基本认证头,其中包含授权在 PyPI 上维护软件包的用户的用户名和密码。
此请求的 HTTP 响应将是以下之一
| 代码 | 含义 | URL 提交影响 |
| 200 | OK | 一切正常 |
| 400 | Bad request | 提交提供的数据格式不正确 |
| 401 | Unauthorised | 提供的用户名或密码不正确 |
| 403 | Forbidden | 用户没有权限更新软件包信息(不是所有者或维护者) |
参考资料
致谢
感谢 Phillip Eby 提供的精确信息和仅通过服务器端更改实现过渡的基本思想。
感谢 Donald Stufft 推动放弃外部托管,并提出为必要的 PyPI 更改和推动过渡阶段 1 的分析工具实现拉取请求。
感谢 Marc-Andre Lemburg、Alyssa Coghlan 和 catalog-sig 团队,感谢他们就摆脱“外部托管”问题进行了深入思考。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0438.rst