Python 打包工具 Setuptools 最近的颠覆性变化
三月底,Setuptools – 一个重要的 Python 打包工具 – 发布了 78.0.1 版本。不到半小时,第一个错误报告 就来了,而且很快就发现,这个变化的破坏性远远超出了预期。在短短五个小时内,78.0.2 版本发布,回滚了更改,并就如何限制未来的破坏性更改所造成的损害展开了多次讨论。尽管如此,许多用户仍然认为响应不够充分。之前发布的一些 Setuptools 版本也造成了一些规模较小但仍值得注意的问题,希望开发人员今后能更加谨慎。但对于 Python 软件包安装程序的开发者、普通 Python 开发者和最终用户,甚至 Linux 发行版的维护者来说,这也是一个教训。
Python 打包
Python 代码通常使用 Python 原生打包系统发布,而不是作为 Linux 包或独立可执行文件发布。其基本思想LWN 以前已经介绍过。软件包有两种标准格式:“wheel ”格式和“sdist ”格式。(或源代码发布)格式。这两种格式都是压缩包,但“[wheel]”格式的压缩包已经预置,除了复制文件外,安装时无需做太多工作。另一方面,sdist 大致代表了开发者的源代码树(尽管可能省略了测试、文档等),而且必须在最终用户的机器上构建。对于包含非 Python 代码的软件包来说,这通常只有用处,但它允许在安装前进行额外设置。sdist 还包括编译非 Python 代码、排列文件以及最终 “构建 ”相应轮子的说明(也许包括将自动运行的 Python 代码)。无论是哪种格式,创建软件包都可能相当复杂。因此,通常情况下,大部分工作会委托给 build backend,它提供了直接从源代码库创建 sdist 或 wheel,以及从 sdist 创建 wheel 的逻辑。安装 sdist 时,pip 等安装程序会检查 sdist 的元数据以确定正确的构建后端,确保后端已安装,并通过标准 API调用它。然后,就像直接下载一样安装生成的轮子。默认情况下,该过程使用一种构建隔离:后端安装在一个隔离的临时虚拟环境中。这确保了构建过程不会相互干扰,允许不同的软件包使用不同版本的 Setuptools,也意味着 Setuptools 不需要提前安装在 pip 自己的环境中。从历史上看,当最终用户需要自己解决和安装依赖关系时,Setuptools 就是各种开发任务的标准工具。如今,它的大部分功能都已过时,但仍可作为构建后端使用,而且出于传统支持的原因,软件包默认情况下都会使用它。
出了什么问题
使用 Setuptools 构建的项目可能会在名为 setup.cfg 的配置文件中描述其部分元数据(该文件使用类似 INI 的简单格式,键值对按章节组织)。尚未发布的 Setuptools 78.0.0 增加了对此类文件内容的更严格验证。在历史上,文件中出现的键名中的连字符和下划线被视为等价,这种行为最初是由distutils 标准库模块实现的。2018 年,一份错误报告 声称,这种规范化会导致tox(一种用于自动化测试等开发任务的 Python 工具)出现问题。Setuptools 开发人员 Jason R. Coombs 同意 存在问题;在对 Setuptools 配置解析代码进行一些修改后,2021 年添加了键名中连字符的弃用警告。Setuptools 第 78 版试图将该警告变成错误。创建新 sdist 的开发者可以很容易地解决这个问题,但这一改动也立即破坏了Python 软件包索引 (PyPI) 上已有的许多 sdist 的自动编译过程。事实上,我们已经知道这一改动与流行的 Requests 库不兼容–它的 setup.cfg 包括 provides-extra 和 requires-dist 两个键,标准名称分别是 provides_extra 和 requires_dist。但是,由于 Requests 只使用 Python 代码,而且已经发布了一个轮子,因此只会给那些坚持直接从源代码构建的用户(如 Linux 发行版维护者)带来麻烦。78.0.1 补丁只是 将 Requests 从 Setuptools 集成测试中移除。Setuptools 开发者 Anderson Bravalheri 也向 Requests 提交了拉取请求,以修复 setup.cfg 的问题(不过事实证明,Requests 的下一个小版本预计会完全更新其打包设置,以避免今后出现任何问题)。然而,带有现在无效的 setup.cfg 文件的软件包比预期的要普遍得多 – 大约有 12,000 个软件包 估计受到了影响。
报告和更新
当第一次有人报告安装失败时,Bravalheri 的初步回应相当不屑一顾:
@andy-maier 你好,请联系软件包开发人员并告知错误信息中强调的问题: […]这个[setup.cfg]中的键名]在 2021 年已被弃用。
他接着 说:
我们把 setuptools 的主要版本从 77.0.3 升级到了 78.0.0,以表明一个有意的突破性改变,即删除了 setup.cfg 中已被弃用的 -\ 分离选项的处理方式。本节对此进行了描述: https://setuptools.pypa.io/en/latest/history.html#deprecations-and-removals.
从某种程度上说,这种回应是有道理的。该项目正确地遵循了其语义版本 政策,而且弃用早在几年前就已发生。原则上,用户只要不升级到新的 Setuptools 版本,就可以避免问题,而开发人员也可以修补他们的 setup.cfg 文件,以满足新的期望。然而,事情并没有那么简单。更新已发布的软件包对每个人来说都是一项繁重的工作–很明显,许多维护者也不喜欢这样的工作。此外,这样做也无法防止有人试图用现在无效的 setup.cfg 文件来安装以前版本的软件包。同时,在用户方面,由于自动安装的所有临时副本,构建隔离使得 Setuptools 难以保持降级状态。一直以来,软件包都无法很好地指定所使用的 Setuptools 版本。现在可以了,但使用 Setuptools 的项目并没有改变任何东西的压力。而且 标准规定的默认值 是使用最新的可用版本。最终的结果是,每一个遵循任何一种过时做法的 sdist 都成了一颗定时炸弹。更糟糕的是,在当今的 “软件生态系统 ”中,项目通常都有大量的传递依赖关系,因此每个软件包的损坏都有可能引发重大的连锁反应。这次被破坏的项目中,有些已经废弃或维护不善,但仍有许多其他项目在使用。例如,stringcase 模块 因变更而崩溃,但它似乎已被遗弃;当我得知此事后,我决定按照现代打包标准重新发布该库。
类似事件
这并不是 Setuptools 主要版本中第一次出现破坏性的改动。在最近的记忆中,最类似的案例可以说是 Setuptools 第 72 版,它 试图删除 setuptools.command.test 模块。这也是对 直接使用 setup.py 运行测试 的 长期弃用 的跟进。这甚至与从 PyPI 安装软件包无关–但由于 setup.py 是 Python 代码,删除该模块会在构建时导致 ImportError。值得注意的是,Requests 项目也受此影响,尽管其开发人员甚至不再使用此工作流。更值得注意的是,Setuptools 77 版原本打算执行项目元数据中引用许可证文件的标准,同时实施新的允许在包元数据中使用 SPDX 许可证表达式的标准。然而,对于一些大型多语言项目(尤其是 Apache 项目),源代码树中的相关许可证文件位于实际 Python 项目的父目录中,这种实现造成了问题。Setuptools 第 71 版 改变了其销售系统 以支持其依赖项的单独安装版本(作为去销售化的垫脚石);当这些依赖项删除了销售版本中仍然存在的功能时,这给用户带来了问题。第 69 版的内部重组破坏了 Astropy 的持续集成(CI)系统,第 70 版的另一个破坏了 PyTorch。并不是每个新版本都会引起大范围的问题,但肯定有可能继续这个列表。
批评
除了 GitHub 的问题,用户还在[黑客新闻](https://news.ycombinator.com/item?id=43462526) 和[Reddit](https://old.reddit.com/r/Python/comments/1jiy2sm/setuptools_7801_breaks_the_internet/) 上批评了事件的处理方式。尤其是,许多人质疑为什么连字符的使用一开始就应该被弃用。事实上,人们可能会推断,Setuptools 无法真正安全地取消对任何东西的支持–尽管开发人员可能很想清理代码,但海勒姆定律 是至高无上的。Setuptools 被视为 Python 的关键基础架构,很明显,Python 世界将在相当长的一段时间内被那些遵循过时打包实践的旧软件包所束缚。有问题的 78.0.1 版本也从未从 PyPI 中删除或 “yanked”(以防止 pip 安装它,除非明确选择确切的版本号),引起了进一步的反对。有人可能会说,Setuptools 已经到了第 78 版,这本身就说明了一个问题–Setuptools 的开发进度太快了。这并不完全公平:大约一半的 Setuptools 主要版本都是在实施新的基于 pyproject.toml\ 的系统之前发布的,第 39 版于 2018 年 3 月发布。不过,我们现在每年仍能看到大约五个新的 Setuptools 主要版本。不用说,将遗留项目更新为现代标准的进展要慢得多。虽然我不希望看到 Setuptools 的开发速度被人为地放慢,但如果能将破坏性的更改捆绑在一起,减少发生的频率,那就更好了。
讨论
但更重要的是,我们需要更好地意识到 Setuptools 中的这些变化,并有更好的流程来处理它们。在 Python 官方讨论论坛上,管道开发者 Damian Shaw 发起了一个 主题,讨论构建后端如何改进其废弃和移除功能的方法,并总结了 Python 生态系统的整体问题:
在这个生态系统中,这意味着任何被严重依赖的构建后端如果做出了向后不兼容的更改,就会在很大程度上破坏用户的工作流程,而可供用户恢复的工具并不完善。
随后的讨论显示,其他一些构建后端的文档建议在项目元数据中设置后端版本的上限。然而,这样做有可能在将来破坏软件包–例如,如果需要更新版本的后端才能在新版本的 Python 下运行。另一种方法是让安装程序使用不同的逻辑来选择构建后端版本。pip 开发者 Paul Moore 最初抵制这种改变:
我不认为这是不可避免的,用户体验设计是我担心的问题之一。在我看来,pip 不应该在这里承担复杂性–我没有看到任何证据表明这是一个大问题。我们在使用其他构建后端时从未遇到过这样的问题。虽然我不想责怪 setuptools,但我也不希望 pip 的功能集被 setuptools 发布管理的失误所左右。
然而,对最初的 Setuptools 错误报告的讨论显示,pip 已经在这里提供了一些有用的功能:允许最终用户限制用于隔离构建的构建后端版本。pip 团队随后讨论](https://github.com/pypa/pip/issues/13300)改进了周边的用户界面,并将其作为官方认可的解决问题的方法。uv明确支持一种类似的方法;诗歌用户在开发环境中安装依赖项时可能需要等待GitHub 问题的解决。Pip 用户也可以直接禁用构建隔离,这在某些情况下可能会解决问题。关于前端工具无法显示警告的问题,Bravalheri 建议 安装程序应该默认向最终用户显示构建警告。这已经不是第一次有人提出 Python 语言的弃用问题了。Python 开发者 Steve Dower指出,最终用户对这些警告其实无能为力。但是,如果一个项目只发布了一个 sdist,那么维护者除非在本地测试安装,否则就不会看到试图构建轮子的输出。即使最终用户无法解决问题,他们也可以在警告显示的情况下发布错误报告。安装程序与构建后端之间通信的最后一个问题是,现有的 API 似乎没有提供一种很好的方法来将警告与构建输出的其他部分区分开来。Moore 建议 为此建立一个单独的系统:
那么,我们为什么不问问 setuptools 的维护者,他们对构建前端有什么要求?现在不行 – 现在情绪太高了 – 但一旦事情冷却下来,我们就应该与 setuptools 合作,了解我们如何能帮助事情在将来更顺利地进行。
他列出了与 Setuptools 开发人员合作的几条建议路径,包括研究如何向用户发出弃用警告:
setuptools 没有办法向用户发出弃用警告?那么前端可以为后端添加一种发布 “优先警告 ”的方法,即使正常输出被抑制,警告也会显示。
希望将来能实现这样的功能。总之,Python 打包系统复杂得令人吃惊–以至于一个关于连字符和下划线的争论就能破坏成千上万的软件包,并引发成百上千的讨论帖子。但是,这种复杂性也给许多方面带来了改善体验的机会。
本文文字及图片出自 Recent disruptive changes from Setuptools
你也许感兴趣的:
- 掌握 Python 3.8+ 中的海象操作符 (:=)
- Python 3.14 中的最佳新功能和修正
- 7 个精妙的 Python 内置命令行技巧,让您的编程更轻松
- Python 3.14 的 3 个语法更新将使您的代码更安全、更好用
- 14 个 Python 高级功能
- Python 的新 t-strings
- Python 异步编程的 9 个级别
- 您不应该再使用的 11 个过时 Python 模块
- Python 中 help() 函数的各种特性
- Python 奇特的自引用
在 PEP517 与传统使用之间,正在酝酿一个更大的问题。Python 生态系统正朝着 PEP517 包装的方向发展,它规范了创建和安装轮子的方式。这有意不处理需要安装非 Python 数据文件和朋友的情况。
然而,这一变化与人们使用 Python `setup.py` 作为 “Pythonic Makefiles”(借用一位朋友的说法)的方式不一致。请参见 https://github.com/pypa/setuptools/issues/2088#issuecomme……,了解最新的爆炸实例。
您能解释一下 “Pythonic Makefiles ”是什么意思吗?我在这个领域(也许是太深入了)已经有一段时间了,但我并不太明白–你想做而又做不到的事情是什么?
我对你所链接的评论线程(https://github.com/pypa/packaging-problems/issues/576#iss……)中的评论的第一印象是,“setup.py install ”乐于将文件放到操作系统所有的目录中,而 pip 则不然,出于这样的原因,“发行版维护者憎恨 pip,因为它不能做发行版维护者想要做的事情”。如果我没理解错的话,这完全是倒过来说的:pip 拒绝将文件放入操作系统所属目录,最初是发行版维护者打了一个补丁,覆盖了上游 pip 的行为,因为他们特别希望 pip 停止覆盖操作系统所属的文件。后来,这一改动被慢慢移植到上游,希望操作系统的维护者能放弃他们的补丁。
该主题稍后提到,“python setup.py install ”用于在 /lib 中安装 systemd 等单元。setup.py 之所以能做到这一点,是因为它只是一个普通的 Python 脚本,但这给了你一条前进的道路;运行普通 Python 脚本的能力并没有被废弃,无论通过普通脚本调用 setuptools 已经废弃了多少。有没有一种方法,可以让您的 setup.py 脚本不使用 setuptools(这是 Python-packaging 特有的工具),而是使用 open(“/lib/…”, “w”) 这样的通用 Python 功能?
(可能需要一个库来处理这样做的一些细节,比如 destdir;我认为可以编写这样一个库,它比 setuptools 简单得多,也有用得多)。
还是我误解了这个问题?
> 我对你所链接的评论主题(https://github.com/pypa/packaging-problems/issues/576#iss……)中的评论的第一印象是,“setup.py install ”乐于将文件放入操作系统所属目录,而 pip 则不然,而且出于这样的原因,“发行版维护者憎恨 pip,因为它不按发行版维护者的意愿行事”。如果我没理解错的话,这完全是倒过来说的:pip 拒绝将文件放入操作系统所属目录,最初是发行版维护者打了一个补丁,覆盖了上游 pip 的行为,因为他们特别希望 pip 停止覆盖操作系统所属的文件。后来,这一改动被慢慢移植到上游,希望操作系统维护者能放弃他们的补丁。
不幸的是……事情比这复杂得多。首先,我想澄清一下你的描述:
Pip 最初乐于将文件 “放入 ”*特定*的操作系统所属目录。具体来说,它会将文件放在任何*Python 环境*中。如果你告诉它为系统环境安装,并赋予它 sudo 权限,它就会乖乖地将文件写入(通常)`/usr/lib/pythonX.Y/site-packages`。如果没有 sudo,它仍然可以写入特定用户的目录;如果以普通用户身份运行系统 Python,Python 仍然可以从那里 “导入”。
最终,发行版意识到,不仅系统级安装会给系统包管理器带来明显的问题,甚至用户级安装也会给系统自带的脚本带来问题。尤其是当你有一个用 Python* 编写的软件包管理器(或其接口),在没有 sudo 的情况下仍能做一些有用的事情时,问题就更大了。因此,https://peps.python.org/pep-0668/(我想这就是你想到的 “上游补丁”)。
但值得注意的是,这一改变与向新的 PEP 517 系统的过渡是*完全相反的。
另外,wheel 格式只能*把文件*放到当前的 Python 环境*–由 `sysconfig` 标准库模块定义。(通常情况下,它不能把文件放到 /etc 或 ~/.config 中,尽管它们并不属于系统。
但是,如果从 sdist 安装并从源代码编译,pip ** 可以有效地将文件放到这些地方(假定有文件系统权限),因为这会请求编译后端做任何必要的事情来制作轮子,包括运行软件包中的任意 Python 代码。在 Setuptools 的情况下,它实际上等同于 `setup.py install`。(至少在我最后一次检查时,使用 sudo 运行的 pip 在调用构建后端时*不会丢弃权限*!)。但这与 Python 生态系统和 Linux 发行版的软件包管理都不相称。
—-
但这条评论说的是完全不同的事情。这并不是说最终用户使用 pip 安装第三方软件包会给发行版维护者带来麻烦。而是发行版维护者*试图使用* pip 来*创建一个系统*软件包。
他们可能会使用 pip 将代码安装到暂存文件夹,然后使用其他工具将代码树打包成 .deb 或 .rpm 或其他格式。如上所述,理论上 pip/Setuptools 组合可以写入文件到 /path/to/staging/etc 或任何需要的地方,即使 Python 环境名义上根植于 /path/to/staging/usr。但这似乎不能与明确提供的 Setuptools 功能一起使用,您必须利用您可以在 `setup.py` 中编写任意代码这一事实,并执行您自己的文件 I/O 等操作。而这些代码可能无法很好地与其他**打包流….。
(另外,pip 并不知道如何跨环境安装。它会根据运行它的 Python 可执行文件来确定安装路径。新版本有一个 `–python` 参数,试图解决这个问题,但它是通过跟踪其他 Python 可执行文件并*生成它来重新运行 pip 代码*)。
我在这里说的不是使用 pip(仅仅调用 `setup.py install` 或其他方法也会出现同样的问题),而是任何软件,如果上游希望使用它们自己的 setup.py 将数据文件安装到标准位置,并且它们的所有文档都引用它,那么它就会停止工作。Python 社区并没有很好地传达这一变化。
但我将更多地回复另一条评论,而不是重复它。
> 能否解释一下 “Pythonic Makefile ”是什么意思?
有很多软件使用 `setup.py` 作为入口点,并依靠它安装到磁盘上的任意位置,使用它通常只是因为软件是用 Python 写的,他们觉得构建系统也用 Python 是很自然的,尽管并不需要这样。
它还经常(如文章所述)处理 testsuite 和其他管理任务,以及 “更复杂 ”的安装。
> 之所以 setup.py 能够做到这一点,是因为它只是一个普通的 Python 脚本 …
正是如此。
> 有没有一种方法,可以让 setup.py 脚本不使用 setuptools(这是一个 Python 打包专用工具),而是使用 open(“/lib/…”, “w”) 这样的通用 Python 功能?
当然可以,但如果软件是通过轮盘安装的,那就行不通了,因为无法将轮盘中的额外数据文件位置映射到系统上的实际位置。而这些依赖这一功能的软件作者似乎都没有意识到这个问题。
也就是说,所有问题都是可以解决的,只是需要在许多不同的项目中做大量的工作,而且我认为他们中的许多人都没有意识到需要这样做。依赖于此的软件一般都不想作为轮子安装(通常是 “用 Python 写的,不是应该导入的库”),它们只是多年来一直捎带着使用 setuptools 和 setup.py 入口点。
>Python 生态系统正朝着 PEP517 包装的方向发展,它正式规定了如何创建和安装轮子。
PEP 517 与其说是定义过程,不如说是提供一个接口,让安装者能够自动完成安装。pyproject.toml “允许你指定一个构建后端,但默认仍然是 Setuptools,而且 pip 几乎可以安装任何为 ”setup.py install “工作流设计的传统软件包。被废弃的*是那个工作流程本身*(https://blog.ganssle.io/articles/2021/10/setup-py-depreca……);但现有的 `setup.py` 不需要修改。Setuptools 基本上是通过模拟 `setup.py install` 所做的工作来实现 PEP 517 合约中的部分内容的;它将继续以这种方式工作,或许会进行一些内部简化。
>这有意不处理需要安装非 Python 数据文件和朋友的情况。
我不明白。滚轮格式明确提供了一个数据文件文件夹,其中有几个不同标准安装位置的子文件夹 (https://packaging.python.org/en/latest/specifications/bin……);Setuptools 明确提供了配置选项,说明要在滚轮中包含哪些数据文件,以及它们属于哪个子文件夹 (https://setuptools.pypa.io/en/latest/userguide/datafiles….)。我不确定数据文件 “非 Python ”是什么意思;如果您的 Python 项目有一个 C 扩展名和一个数据文件,那么无论是滚轮格式、Setuptools 还是安装程序,都不会关心是 Python 代码读取数据还是 C 代码读取数据(或者两者都读取)。
但是,如果一些开发者使用更高级工具中的 `setup.py`,或想将文件安装到 Python 环境之外的地方(如 /etc 或其他),他们的工作就会被打乱。或者我猜,如果他们试图使用 Setuptools 制作 .rpm 或 .deb 等文件。这些最终都会消失。
但替代方案是存在的,而且无论如何,这都比最终用户因为无法从本地源代码构建传递依赖关系而导致安装失败的破坏性要小得多。
每次我试图在电脑上做任何事情时,都会莫名其妙地遇到 Python 打包问题。为此,我对 Python 语言怨恨至极。
附议。我讨厌的是,如果你的 Python 安装来自操作系统软件包,你就不能使用 pip3 来轻松安装全系统的软件包,而且它还要你设置一个 venv。这让我完全抓狂。
> 我更喜欢 Perl 而不是 Python,Perl 可以让你同时安装操作系统软件包和来自 CPAN 的软件包,并将它们放在不同的目录下。为什么 Python 做不到这一点呢?
因为 Python 支持 “将它们放在不同的目录中”,这就是所谓的 venv。
> 但是 pip3 非常严厉地警告我们不要这么做。
是的,没错。
因为 Python 支持 “将它们保存在不同的目录中”,这就是所谓的 venv。
如果是为单个用户安装,则称为 pip install –user。
如果你要为系统中的所有用户安装,那么你就处于三种情况之一:
* 你是发行版。那么这种行为一开始就是你的主意,因为上游 Python 不会这样做(即告诉 pip 安装失败,并给出一个错误,提示不要篡改软件包管理的文件)。如果你不喜欢这种行为,就不要把它放到你的发行版 Python 中。
* 您是系统管理员。那么您可以使用站点模块的配置钩子来设置和自定义模块的导入方式和安装位置,然后使用 pip install –prefix 或 pip install –target 手动将软件包安装到其中。你的发行版可能已经为你配置了这样的功能(使用 python -m site 查看)。请参见 https://docs.python.org/3/library/site.html
* 您正在构建容器镜像或类似镜像。在这种情况下,你实际上已经任命自己为容器的系统管理员(见上一条),或者你正在使用自己实际使用的软件从头开始构建容器(见第一条)。如果你不想当系统管理员,那就使用虚拟机而不是完整的容器–虚拟机比容器便宜得多,也简单得多!
我支持方案 2–系统管理员。
> 任何使用 ./configure 构建的普通 C 或 C++ 程序都会默认安装在 /usr/local/,并且不会干扰系统软件包。
通常不会干扰!的确,它不会物理覆盖文件,但如果它安装了 bash、perl 或 python 之类的二进制文件,然后在 PATH=/usr/local/bin:/usr/bin 时破坏了现有的系统软件包呢?
如果它在同一个 SONAME 下安装了更新的、不兼容的、有错误的、配置不同的共享库怎么办?/usr/local/lib 是默认的共享库搜索路径。
如果你的 Perl 软件包安装了一个更新的、不兼容的、有漏洞的、配置不同的 Perl 库,并首先出现在 @INC 上,从而破坏了操作系统提供的某些 Perl 命令,那该怎么办?至少在我的系统中,/usr/bin/perl -e ‘print “@INC/n”’在 /usr 之前显示 /usr/local。
Python 也是这样做的。它会在 /usr/local 中查找 Python 代码。直到今天,它仍然很乐意用(例如)“sudo pip install –break-system-packages ”安装到那里,如果你愿意承担这种风险,它的存在只是为了让你选择承担这种风险。我无法很好地解释为什么这个问题对 Python 的影响似乎大于对 C 语言用户和 Perl 用户的影响。我最有可能的猜测是,安装第三方软件包的 Python 用户要比安装第三方软件包的 C 或 Perl 用户多得多,而且他们不太可能是系统管理员,因此在 Python 中出现这些不太可能但肯定可能出现的情况要常见得多。
造成这种窘境的一个重要因素是,许多著名的发行版都搭载并依赖于用 Python 编写的工具(包括软件包管理器)。安装发行版已经安装的不同版本的软件包可能会破坏这些工具,结果并不乐观。
我相信有些发行版已经考虑过让发行版的工具使用完全独立的 Python 安装,这就带来了复杂的问题(比如用户使用软件包管理器确定安装了 “requests”,但却抱怨 “import requests ”在自己的程序中失效)。
这是一个令人遗憾的情况,但也导致一些人(包括我)除了临时或一次性脚本外,再也不使用发行版提供的 Python 了。
但如果安装了 bash、perl 或 python 之类的二进制文件,然后破坏了现有的系统软件包怎么办?
你认为 bash 是一个糟糕的、被破坏的软件包?)
我想问题的一部分在于 Python 软件包管理器也会安装软件包的依赖包。所以,是的,我同意,如果 `wget http://ftp.gnu.org/zardoz.tgz && tar xf zardoz.tgz && cd zardoz && ./configure && sudo make install` 导致 /usr/local/bin/bash 存在,这是可怕的、坏掉的、完全不能接受的。但是,“sudo pip install zardoz ”很可能会引入一个更新版本的依赖项(当然,可能不是 bash,但有很多用 Python 写的 CLI 工具,偶尔也需要一些不是用 Python 写的 CLI 依赖项)。
另一种看法是,如果需要某个常见依赖项的更新版本,像 automake 这样的东西就会让你自己找出答案,这时,作为系统管理员,你会在把更新版本的 bash 安装到 /usr/local/bin 之前思考一下这是怎么回事。Python 生态系统不希望让您自己解决这个问题。两种理念都有很好的论据,但这也是为什么安装 Python 软件包的人比输入过“./configure ”的人多得多的根本原因。
Python 软件包管理器也会安装软件包的依赖包。
> 即使是非 Python 依赖项,它们通常是在 Python 软件包仓库之外发布的?
呃……算是吧,但也不尽然。安装真正的非 Python 依赖项的事实标准工具是 conda,但 conda 是完全不同的东西,它有自己独立的软件包仓库等,大多数发行版要么根本不打包,要么至少默认不安装。Conda 还设置了一个完全独立的 Python 安装,因此它实际上能与系统 Python 玩得很好(从它通常完全不接触系统 Python 的意义上来说)。
pip (而不是 conda)的问题在于,在实践中,有大量用 Python 编写的库,包括许多非 Python 库的 Python 绑定。如果没有 venv 或 –user 激活,安装任何依赖于这些库的 Python 软件包都会导致它们被安装到 /usr/local,然后大量的系统软件包可能会被破坏。
> 总之,我最近在用 Python 做一些事情,只是使用系统 python。我一直在考虑研究非系统 Python 的东西,甚至打包我的一些代码,但被众多的 Python 打包系统挡住了,而且在使用哪种打包系统上明显缺乏共识。我想我还是放弃这些系统吧,换一种语言。或者改行养羊。
温馨建议 学习 uv、venv 和 *maybe* pip,忽略这个领域的其他所有语言。其实你并不需要了解 venv 和 pip,因为 uv 可以处理这两个用例,但从概念上理解它们还是有好处的,因为它们是 Python 最接近标准软件包工具的东西。
理由:uv 能很好地处理 90% 以上的用例,与大多数其他工具相比,它的性能非常好,得到了积极的支持和维护,比起脚本,它更偏向于正确性(尤其是在如何解决依赖关系方面),而且并没有严重偏离大多数其他工具所遵循的事实上的最佳实践(所以如果 uv 突然被另一个新的 Shiny 所取代,你可能不需要花费大量精力就能从它那里迁移过来)。
我以前听说过 Poetry 和其他一些解决方案的好话,以前我可能会向你推荐它们,但如今,普遍的看法是 uv 的高性能将大多数竞争者远远甩在了后面,以至于除非你真的需要 uv 所缺乏的功能,否则接触任何非 uv 工具都没有太大意义(当然,或者你的项目已经在使用其中的一个替代方案)。如今,uv 缺失的东西已经不多了(至少据我所知是这样),所以使用其他工具的理由也越来越少了。
90% 的比例太乐观了: uv 在很大程度上嵌入了生态系统的 webdev 部分(与 pipenv 和 poetry 属同一产品线),因此能很好地解决他们的需求(uv 所做的假设是其提速的主要来源,但也会造成问题)、 但在 webdev 之外,它增加了 linux 发行版打包的难度(因为现在你有两种语言要管理),由于其假设,它不能很好地与数值生态系统配合(因此使用了 conda),而且它试图掩盖管理安装时对设计的真正需求(例如,看看它是如何安装的)。 例如,看看它是如何安装 python 的,以及由于试图实现几乎静态的安装而导致的所有小问题)。如果你是它的目标群体,那么它比 pipenv/poetry 有了很大的改进(因为它们做出了相同的假设,并帮助铺平了通往 uv 的道路),但除此之外,对我来说,它似乎是分裂 Python 生态系统的另一个楔子。
> 由于其假设条件,它不能很好地与数值生态系统配合(因此使用了 Conda)。
上周在 PyCon 上,我和 Conda 人员以及其他用 Python 进行数值/科学计算的人员共处一室,试图为基于 PyPI 的生态系统(pip,现在还有 uv 和 poetry 等)解决这些问题,并试图以一种我们与 Conda 生态系统兼容/他们可以采用相同设计的方式来解决这些问题。老实说,在过去几年中,PyPI 生态系统在完成这项任务方面表现得相当不错,比十年前的 “pip install numpy ”总是试图从源代码构建要好得多。英伟达(NVIDIA)在 2019 年大张旗鼓地退出 PyPI 生态系统(https://medium.com/rapids-ai/rapids-0-7-release-drops-pip……)之后,在过去几年中又恢复了对 PyPI 生态系统的一流支持。虽然还有很多事情要做,但我们应该已经到了这样的地步(根据我的经验,现在已经到了这样的地步):大多数人,当然也包括大多数个人,都能很好地使用 pip/uv/等工具,所以我真的很好奇,你个人认为哪些事情是行不通的。
> 我很好奇你个人认为哪些事情是行不通的,而且它试图掩盖在管理安装时对设计的真正需求(例如,看看它是如何安装 python 的,以及由于试图实现几乎静态的安装而导致的所有小问题)。
我是这些 Python 构建的维护者之一,我绝对愿意接受 bug 报告,无论是在这里还是在我们的 bug 跟踪器上。处理这些小问题基本上是我目前的首要任务。请告诉我,我喜欢有更多的测试用例。)
我想区分底层标准/工具和更高层次的封装(诗歌和 uv 就是其中的代表,我对 90% 的封装都能正常工作有意见)。
我会把这些东西归类为从剪纸到阻断器的一系列产品。https://gregoryszorc.com/docs/python-build-standalone/mai……上列出的东西接近于 “剪纸”,因为它们可以通过使用不同的构建方式来避免(甚至可以从源代码构建,而且它们实际上是有文档记录的),但当 uv 等工具默认隐式使用它们时,它们就开始向 “阻塞 ”的方向发展(因为人们倾向于使用默认设置,所以这就变成了一个层层调试问题的问题)。一旦某些集成开发环境开始介入,你就不得不调试配置不佳的机器,并浪费数小时或数天的时间进行修复。
uv 确实不是为此而设计的(诗歌也不是,它们对轮子的假设太多了),虽然你可以(而且我已经)创建自己的私有索引来控制安装程序所看到的内容,但使用 pip 要简单得多(而且不容易出错)。
还有一些有趣的地方,比如 PyPI 打包的 Jupyter 插件直接无法运行(尽管 conda 版本可以运行),这是因为轮子无法安装到特定路径(因为它们需要与所安装的系统集成)。
这一切都源于这样一个假设,即预置的 PyPI 相关二进制文件(具有各种限制)是用户需要正确设置机器的一种解决方案(而非变通办法)。这一点值得与 R 进行比较,R 在 Windows 和 MacOS 上的安装程序会设置一个适当的开发环境(包含编译器),因此不会出现此类问题。
谢谢你,这对我很有帮助,我很感谢你花时间写这篇文章!该文档中的一些怪癖现在已经没有了(例如,musl 的构建现在是动态二进制,而不是静态二进制,因此扩展可以正常工作);我将更新它。
> 一旦某些集成开发环境开始介入,现在你就不得不调试配置不佳的机器,并浪费数小时或数天的时间来修复。
我想澄清一下,你的意思是人们使用集成开发环境来设置 uv,从而设置了 python-build-standalone ,这就增加了调试的难度,因为很难弄清楚他们的设置是什么?还是说在集成开发环境中使用与在 CLI 中使用有什么不同?
> 还有一些生态系统中不能使用预编译二进制文件的部分(例如 MPI),因为需要链接到单个 BLAS,你最终会开始添加“–no-binary ”之类的标记,并重建半个生态系统。
这肯定是我们上周末花时间讨论的问题之一(https://pypackaging-native.github.io/key-issues/abi/ 上也提到了这一点,这是一个由从事科学 Python 生态系统工作并希望非二进制生态系统运行良好的人编写的优秀网站)。这当然是目前的不足,但也是整个科学 Python 社区的当务之急。虽然我相信大家都有具体的例子,但如果您有特别喜欢的预编译二进制轮子或软件包名称不能很好地工作,或者需要共享一个 BLAS 的库组合,我还是喜欢有更多的测试案例。)
> […] uv 确实不是为此而设计的 […] 使用 pip 要简单得多(也更不容易出错)。
uv 和 pip 都会从相同的地方安装相同的包,而且当你使用 –no-binary 从源代码构建时,无论是使用 uv 还是 pip,你都可以链接你的系统库,所以我对此很好奇–这是因为你可以使用 venv –system-site-packages(或者以某种方式使用 pip 而不使用 venv)并从操作系统获取一些编译过的 Python 包,而 uv 不支持这一点吗?还是其他什么原因?
> 还有一些有趣的问题,比如 PyPI 包中的 Jupyter 插件直接无法运行(尽管 conda 版本可以运行),原因是轮子无法安装到特定路径(因为它们需要与所安装的系统集成)。
这对我来说是个新问题–你有不能工作的特定插件/示例命令吗?(我知道有很多人都在使用 pip + venv 在生产中使用带扩展的 Jupyter,所以我假设这不是 “任何扩展”)。
> 这一切都源于这样一个假设:预编译的 PyPI 相关二进制文件(具有各种局限性)是用户需要正确设置机器的一种解决方案(而不是变通办法)。这一点值得与 R 进行比较,R 在 Windows 和 MacOS 上的安装程序会设置一个适当的开发环境(包括编译器),因此不会出现这类问题。
是的,前提条件是要真正建立一个带有编译器和库依赖关系的适当开发环境,而这本身就有很多问题。) 毕竟,在十年前,这还是 pip 的现状,而事实上,这种糟糕的体验既是 conda 出现的原因,也是 pip 开始做轮子的原因。(除非你的意思是 Python 或 uv 或其他安装程序应该安装它自己的编译器工具链,与主机上的编译器工具链无关,并从源代码构建东西,包括像 BLAS 这样的 C 库?这个想法很有意思….) Conda 支持 R 和 Python,我的印象是它对 R 的支持很受欢迎,原因大致相同,那就是从源代码构建所有 R 软件包是一种困难的经历。
> 为什么脚本语言需要二进制文件?
有两个原因。其一,它是一种通用编程语言,不仅擅长脚本编写,还擅长其他方面。例如,它在科学计算方面的应用非常广泛(甚至在当前的人工智能热潮之前),而且用它做的一件极其常见的事情就是安装 BLAS/LAPACK 的封装器并使用它们。(大多数 Python 程序员甚至不知道他们使用的东西的核心是 BLAS/LAPACK)。
其次,脚本语言经常做的事情是调用二进制文件!Graphviz 就是一个很好的例子–常用的 Python 绑定程序会调用 “dot ”作为子进程。是的,你可以自己安装它(你从 pip 获得的版本也希望你这么做),但有充分的理由需要一个与包装器等测试过的版本相匹配的版本。
(另外,如果你坚持从操作系统中获取这些东西,就意味着你需要用 sudo 来安装它们。奇怪的是,Python 生态系统可能是 Linux 上二进制文件最好的完全非特权软件包管理器。我并不是说它很好,我们还有很多工作要做,但其他选择并没有比它更好)。
> 我一直在考虑研究非系统 Python 的东西,甚至打包我的一些代码,但被众多的 Python 打包系统挡住了,而且对使用哪种打包系统明显缺乏共识。我想我还是放弃这些系统吧,换一种语言。或者改行养羊。
别让我阻止你去养羊,如果你愿意接受我的建议,那就试试 uv (https://docs.astral.sh/uv/)吧。(我推荐使用提供的安装程序,但如果你的操作系统有相对较新的版本,也可以使用)。uv 的做法是将所有虚拟环境的东西都抽象出来(实际上是将其视为一个实现细节)。
事实上,你可以写一个单文件的 Python 脚本,其中包含一些关于其依赖关系的元数据,然后使用 “uv run myscript.py”,如果它没有这些依赖关系,它就会在幕后创建一个临时的虚拟环境,然后运行你的脚本,完全不受操作系统的影响。
uv 还能安装自己的 Python 版本,通常比操作系统的版本要新得多,这进一步增强了它对操作系统的独立性。我是这些 Python 版本的维护者之一,所以如果你对它们的构建方式有什么不满意的地方,可以随时骂我。你这么做可能是有道理的,因为肯定有几个问题还没解决。) 但它运行得很好,而且避免了与操作系统冲突的所有问题。
uv 相对较新(一年多一点,而这些功能的一半还不到一年)。它正在迅速获得共识,而且我认为,无论这个软件项目是否是未来的发展方向,人们已经达成了强烈的共识,即类似 uv 的东西才是正确的方法。但这也是为什么你还没有看到共识的原因。在过去的几年里,我们做了大量的工作来改进 Python 的打包。这些工作已经初见成效,但也只是刚刚开始。尤其是单文件,它的宣传力度非常不够,我认为这可能是大多数系统管理员(我自己也是其中之一)希望采用的方式。
根据您正在做的事情,我不确定使用另一种语言是否会更好。如果您正在编写的 Python 足够有趣,已经超出了标准库提供的范围,尤其是如果您正在做一些需要编译库(如 BLAS/LAPACK)的事情,那么依赖性问题就必须以某种方式来解决。(顺便提一句,我很好奇你安装的是哪种依赖关系)。或者换一种说法–在你的操作系统上有足够多用 Python 写的东西,“sudo pip install ”会给它们带来风险,这是有原因的。尽管你看到了这么多问题,但实际上它还很好地解决了另外几个问题。
我在使用 uv 的 python 托管安装时遇到了一些问题,尤其是 netaddr 软件包,这确实是一个未经维护的怪兽,但也是一个足够常见的需求(尤其是在我工作的 OpenStack 环境中),我没有太多选择。最后,我只使用了系统 python 和发行版的 python3-netaddr 软件包,因为我找不到办法让托管的 python install 成功地从源代码构建 netaddr(netaddr 的错,不是 uv 的错),或者提供某种方法将我自己的构建注入环境。
不幸的是,我并没有记录我具体做了什么,而且这是在我之前的机器上,所以很难回去重新运行以给出一份合适的错误报告,或者看看在那之后的大约 6 个月里事情是否发生了变化(鉴于 uv 的发展速度之快,这是很有可能的)……。
我想说,解决未维护或维护不善的软件包问题并不是 Python 打包社区应该负责的事情 … …确实不是 … …但失败是有程度的,有的比其他的更优雅,我感觉就像撞上了一堵砖墙,而不是什么更包容的东西。很明显,python 托管安装是一个非常精心设计和控制的环境,你能提供的灵活性是有限的,但这有时也会让它们变得不可用,尤其是像 netaddr 这样的奇葩情况。
哈,我似乎选错了尝试使用 netaddr 的时机,因为它显然比我遇到这些问题时更活跃、维护得更好……. . . 当然,这并不能改变更广泛的观点,但不注意到这些变化对 netaddr 开发人员来说是不礼貌的…….
你确定你想的是 netaddr?那是一个超级简单的纯 Python 软件包,所以构建它应该是轻而易举的事,而且它有独立于平台的轮子,所以你甚至不需要这样做。
为了完整起见,我刚刚检查了 `uv run –with netaddr python`,然后锁定了各种最新和不太新的版本,包括 Debian stable/oldstable 中的版本,它们比 uv 本身还要老。我还尝试使用 `–no-binary` 来强制本地构建。看起来一切正常。
(你是指 netifaces 吗?那个软件包有编译过的代码,构建系统也很糟糕,但它似乎也能在 uv 下正常工作。我第一次尝试的时候确实失败了,因为我的 PATH 路径中没有 C 编译器,第二次尝试也失败了,因为 netifaces 缓存了它的 configure-esque 检查,而 uv 会保留这些检查,但如果把它们清除掉再重试,就能正常工作了)。
关于旧版本,我认为这是一个合理的期望,前提是实际代码与当前的 Python 版本兼容。有时您可能需要在构建环境中使用旧版本的 setuptools(回到本文的主题),也许这个过程需要更好的文档和错误信息,但这种事情应该是可以做到的。
是的,在一个慵懒的周六早晨,咖啡因不足让我把 netaddr 和真正的罪魁祸首 netifaces 弄混了。如果现在能用了,那很可能是因为我遇到这个问题后进行了修改–uv 的开发速度几乎和运行速度一样快,很难跟上。也可能是使用的 Python 版本比 netifaces 想要的版本更现代?老实说,我记不清细节了,而且我必须做很多工作才能重建足够的上下文来尝试复制这些问题。
我认为,使用系统 python 软件包解决某些依赖性问题,可能是通过将它们导入托管的 python 安装中,或者导入 uv 构建的 venvs 中。显然,假设你使用的是足够兼容的版本(这可能很难弄清)…………. 这并不是你想经常使用的东西,而且需要针对每个软件包进行明确选择,但如果问题软件包在依赖关系树上有一段距离,而且无法解决,这可能会很有用。
呃 . 我显然是个咖啡没喝够的白痴–我遇到问题的软件包是 netifaces,而不是 netaddr。如果只是为了减少我的尴尬,编辑注释绝对是有用的. . .
任何以你描述的方式破坏系统的软件,如果不能迅速修复,就会发现自己不再被使用。
> 任何软件,如果以你所描述的方式破坏了系统,并且没有得到迅速修复,都会发现自己不再被使用。
正确。这就是为什么它实际上是以我描述的方式被修复的原因。)
> 你提到的问题在实际应用中几乎从未出现过。
在实践中确实存在。我是 Python 标准文档的主要作者和发起人,正是我使得“–break-system-packages ”得以实现。我们这样做是有原因的,Python 生态系统打包工具的开发者和为 Linux 发行版重新打包 Python 的开发者都参与其中。
一般来说,你有这个权利,但请注意,同样的 “外部管理环境 ”保护系统也适用于 `pip install –user` 目标位置 – 参见 https://stackoverflow.com/questions/75608323 。引自 PEP:
> 发行版用户可用的 python3 可执行文件与发行版中其他软件依赖的 python3 可执行文件通常是相同的二进制文件。这意味着,如果最终用户在虚拟环境之外使用 pip 等工具安装 Python 软件包,那么发行版提供的 Python 语言软件也能看到该软件包。如果新安装的软件包(或其依赖包)是通过发行版安装的软件包的更新、向后不兼容的版本,则可能会破坏发行版发布的软件。
> 这可能会给发行版的完整性带来严重问题,因为发行版通常都有用 Python 编写的软件包管理工具。例如,使用 pip install 命令就有可能无意中破坏 Fedora 的 dnf 命令,导致难以恢复。
> 这既适用于全系统安装(sudo pip install),也适用于用户主目录安装(pip install –user),因为无论哪个位置的软件包都会显示在 /usr/bin/python3 的 sys.path 上。
> 我猜如果你掌握了正确的魔法就能做到,但 pip3 非常严厉地警告你不要这么做)。
这不是魔法。只是一个标记。
sudo pip install –break-system-packages […]。
问题是,现在你的系统软件包管理器并不知道你背着它,用第三方工具在 /usr 安装了一些乱七八糟的东西。往好了说,这会让你分不清哪些文件由系统管理,哪些由你手动运行 pip 来 “管理”。最糟糕的情况是,这会导致某些系统软件包被破坏,因为它们是针对 libfoo 的 X 版本进行测试的,而你安装的是 Y 版本。Pip 告诉你不要这么做是完全正确的–它会以不可预知的方式破坏你的系统,这就是为什么它被称为 –**break**-system-packages 而不是 –global-install 或其他听起来无害的东西。
Pip 以前的工作方式与你描述的完全相同(如果以适当权限调用,则会静默覆盖整个系统的软件包)。因为太多用户不小心破坏了他们的系统,所以才对它进行了修改(正如当时在 https://peps.python.org/pep-0668/#motivation 中讨论的那样)。因此,现在只有当你要求它这样做时,它才会破坏你的系统,这在我看来是一种胜利。
另外,我知道你确实需要破解操作系统的软件包管理器,因为如果没有,你就不会收到错误信息。Pip 会在软件包目录中查找一个名为 EXTERNALLY-MANAGED 的文件,而这个文件并不与 Python 上游一起发布,所以只有当你的发行版把它放在那里以指示 Pip 不要去碰它时,它才会存在。换句话说: 你的问题是你和你的发行版之间的问题,而 pip 只是个信使。
下面是一些不那么糟糕的替代方案:
* pip install –user […], 安装到 $HOME 的子目录。
* 创建一个 venv。尽管 Debian 用户需要安装 python3-venv,但使用 venv 并不困难。从字面上看,它只是一个包含 Python 二进制文件副本(或指向 Python 二进制文件的 symlink)的目录,外加一些脚本工具,告诉 Python 和 pip 将该目录作为安装根目录(而不是 /usr/lib 下的某个目录或目录组合)。
* 使用操作系统软件包管理器来安装所需的操作系统软件包。Pip 不支持自动安装,大概是因为它不想与 APT、RPM 和 Pacman 等集成,也不想把 PyPI 中的软件包名称转换成操作系统软件仓库使用的名称。别问我 Perl 是怎么解决这个问题的,我不是 Perl 人。
* 配置 Python,使其在 /usr/local 下有一个单独的软件包目录,在这里你可以做任何你想做的事,而不会破坏操作系统的软件包管理器,这样 pip 就不会抱怨系统范围内的安装。有关内部工作原理,请参见 https://docs.python.org/3/library/site.html 和 https://packaging.python.org/en/latest/specifications/ext… 。
还有一个 “INSTALLER ”元数据文件,它可以(应该)用来记录特定 python 软件包的安装情况。我认为,Fedora-likes 和 Debian-likes 也会将其软件仓库中的 python 软件包安装到一个路径 (/usr/lib),然后配置 pip 安装到另一个路径 (/var/lib),并将两者都放在 Python 搜索路径中。在全系统范围内安装仍然会破坏实用程序,但只需取消全系统范围内的 pip 安装,就能让发行版提供的工具重新正常工作,这要容易得多。
在其他一些发行版上,可以而且确实……。向你的 python/setuptools 打包商投诉吧。
> Perl 可以让你同时安装操作系统软件包和来自 CPAN 的软件包,并将它们放在不同的目录中。为什么 Python 做不到呢?
Python 不进行任何安装。整个打包系统和软件包生态系统与核心语言开发保持着一定的距离。
像 pip 这样的安装程序可以并且确实安装了来自 PyPI 的软件包,这些软件包与 Linux 发行版提供的 Python 软件包是分开的。不仅如此,它还有独立的系统级和用户级安装。系统包管理器将使用(通常)/usr/lib/pythonX.Y/dist-packages。为系统安装 Python 时,如果使用 sudo 运行,pip 将使用 /usr/lib/pythonX.Y/site-packages,否则将使用 ~/.local/lib/pythonX.Y/site-packages。
你的发行版现在坚持使用虚拟环境的原因是,*这种文件夹安排仍然存在问题*。发生的情况是,用户安装的模块会对系统脚本试图使用的系统提供的模块产生阴影。只有在以用户身份运行系统脚本时,~/.local 软件包才会可见,但*这仍然会造成足够多的问题*,以至于发行版认为 “不给 pip sudo 权限 ”的保护措施还不够。
我真的不明白,为什么 “轻松安装*全系统*软件包 ”会如此重要。但你完全可以使用 venvs(当然要使用 sudo)来实现这一功能,例如:在 /opt 内的某个地方创建一个 venv,然后在那里进行安装,如果是一个带有驱动脚本的 “应用程序”,甚至可以将其从 /usr/local/bin 以 symlink 的方式链接到 /usr/local/bin。事实上,较新版本的 pipx (https://pipx.pypa.io/stable/) 就为应用程序封装了这一过程(使用 pip 的共享 vendored 副本)。我在个人博客文章中对此有更多介绍:https://zahlman.github.io/posts/2025/01/07/python-packagi…
将虚拟环境看作是 Python 的一个独立安装可能会有帮助,因为它与 Python 的安装很接近,只是有指向 Python “基本 ”可执行文件的 symlink 和对标准库的特殊访问,而不是全部复制。如果你因为要记住 “激活 ”虚拟环境而苦恼,请记住这实际上作用不大。一般来说,只需提供虚拟环境的 `python` 符号链接路径即可。
有趣的事实:如果发行版想支持–user,要做到这一点并不难。您只需确保任何需要 Python 的系统包都设置了 -s 标志(在 /usr/bin/python 上)或 PYTHONNOUSERSITE 环境变量(任意值),然后用户主目录中的任何内容都会被静默忽略。我猜想这里会有不同程度的困难:
* 对于由你正在使用的任何 init 管理的东西(我们不要在这里挑起 systemd 的圣战),这应该是小事一桩。
* 对于由 systemd –user(或任何等价物,以及 GDM/KDM/login/getty/etc.等与会话相关的东西)管理的东西,这应该也是小事一桩。
* 剩下的就是用户运行的二进制文件了。如果是通过桌面 shell 运行,可以在 .desktop 文件中进行配置;如果是通过命令行运行,可以在 Shebang 行中进行配置……除非您使用 #!/usr/bin/env python 技巧,在这种情况下,您已经用完了一个参数,无法将标志传递给 Python(或者,要求 env 为您设置环境变量)。但如果你是发行版,你应该知道 Python 安装在哪里,而不需要使用 /usr/bin/env。
我想他们在实践中不这样做的主要原因是 “shebang”。所有其他的东西都可以合理地使用环境变量,这更可取,因为它可以正常工作,而且不需要到处编辑 /usr/bin/python 命令行标志。另一方面,shebang 需要进行大量编辑,而且还存在不能被子进程自动继承的缺点。因此,这取决于发行版是否愿意花时间来支持它。
> 我更喜欢 Perl 而不是 Python,Perl 可以让你同时安装操作系统软件包和来自 CPAN 的软件包,并把它们放在不同的目录下。为什么 Python 做不到这一点?
我使用的 perl 也比 python 多得多,主要是在 RHEL 类型的机器上,我不会以 root 身份运行 CPAN 来安装系统范围内的文件,而是制作 RPM。perl 不需要 python-wheel 风格的软件包格式,因为 RPM 与 perl/CPAN 生态系统的集成非常出色,而且通过直接使用系统范围的软件包数据库(包括依赖关系管理),你不会因为两个不同的生态系统的部分依赖关系图而互相争斗并破坏东西,你只需使用正常的 RPM 机制来检测和解决冲突。据我所知,所提供的用于传递 Makefile.PL/Build.PL cli 选项的 rpmmacros 非常全面,所提供的脚本使用简单的 regex 匹配来可靠地收集入站和出站的运行时依赖关系,其中包括 /^(use|require) (.+)/ 和 /^package “(.+)”/ 以及由 `cpanspec` 模板化的更具体的构建步骤。有一个将 perl 包映射到 RPM 包和库依赖名称的标准命名约定,例如 perl lib `Foo::Bar` 就是 rpm `perl-Foo-Bar` 并提供了 `perl(Foo::Bar)`。python setuptools 可以创建一个 RPM,但它并不会将 pip 用于解决依赖关系的任何数据填充到 RPM 中,而且 RPM 的构建宏/脚本似乎也没有办法解析导入语句以生成该列表,或者在两个系统之间进行映射的约定方法,如果有更好的工具可以向 RPM 提供与 pip/uv 相同的信息,我还没有找到。
我对这两个生态系统的原生打包元数据格式都不够了解,无法对它们进行全面的比较和对比,我也不使用它们,我只是使用 RPM 构建我的内部 perl,让内置的 rpmbuild 脚本来处理,但我发现为全系统安装而构建 python 包要复杂得多、 但我发现为全系统安装而构建 python 软件包更为复杂,而且我从未找到像 `cpanspec` 或甚至 `rpmbuild-newspec -t perl` 那样好的工具,以至于我几乎放弃了它,而只是将整个 venv 打包成一个 RPM,让其他脚本可以依赖,而不是为依赖树的每个部分制作单独的 RPM(对于 NAPALM 而言)。我不知道是不是因为 python 和 pypi 的运行规模比 cpan 大得多,所以问题出现得更频繁,还是因为向后兼容性的文化非常不同,但在使用 perl 时,我很少或从未遇到过应用程序与其他系统范围内已安装的实用程序共享依赖关系不兼容的情况,甚至从未遇到过过去 10-15 年中有重大变化的库、 但使用 Python 时,我肯定要非常小心,因为依赖树中的某些库出现不兼容的新版本并在运行时破坏应用程序的情况要常见得多,甚至在构建虚拟环境时,我有时不得不屏蔽库版本,直到其他部分跟上。我还遇到过这样的情况:在一个版本超过 6 个月的虚拟环境中重新安装一个 python 应用程序会变得很困难,除非你冻结了一个列表,列出在应用程序最初发布时整个虚拟环境库集的确切可用版本,因为基本上可以肯定的是,在这段时间内会有一些不兼容的变化,而应用程序将无法运行,直到你找出需要将哪些库的版本固定下来,如果你运行一个应用程序的时间超过 6 周的冲刺周期,这将是非常乏味的。也许我的旧 perl 代码只是依赖于停滞不前、无人维护的库,它们之所以稳定,是因为它们很旧、没有变化,但我们使用的大多数库都在 RHEL 或 EPEL 中,所以它们一定还在被常用软件积极使用,而且我们还没有遇到像使用 python 时那样的问题,但我无法明确说明真正的原因。
我不介意对 CPAN 与 PyPI 和 NPM 进行更深入的比较,看看它们各自解决了哪些不同的问题,以及原因何在,CPAN 是 OG,也许他们学到了如何进行大规模库管理的方法,也许存在语言支持差异,这意味着一个生态系统中的相同解决方案由于技术原因无法转换到另一个生态系统中,也许存在长期的政策差异,排除了解决这些问题的某些技术解决方案。我不知道,但我肯定愿意了解更多这方面的信息。
“如果有更好的工具能像 pip/uv 一样为 RPM 提供信息,我还没有找到”。
当然有。现代的 Fedora(和 RHEL,至少是 10.x,我不记得 9.x 中有多少)Python 软件包–有合理的上游–可以自动生成编译和运行时依赖关系并提供。Fedora Python 软件包总是为其包含的每个模块提供 “python3dist(modulename)”,这是自动生成的。
https://docs.fedoraproject.org/en-US/packaging-guidelines… 是现代打包指南中的样本规范。您可以看到其中的内容很少,因为很多都是自动完成的。
……停止的唯一办法就是不开始。
他们怎么会弄坏了 12000 个包装而不自知呢?
好吧,这就是视角的问题了。
PyPI 自己就托管了大约 630.000 个软件包。你说他们破坏了 12000 个软件包却没有注意到。我认为 setuptools 只破坏了所有已知软件包的百分之二。
现在,要找到这百分之二的软件包可能有点困难。我不认为 Python 有任何基础架构可以在整个生态系统中进行大规模的自动重建,以测试这种回归。
考虑到这一点,再加上 Python 打包和工具的多样性,他们只破坏了 12000 个软件包,这让我感到很惊讶。
>现在,要找到这 2% 的软件包可能有点困难。
他们自己的一个依赖包也因为这次变更而损坏了:
> 事实上,众所周知这一修改与流行的 Requests 库不兼容 […] 。
> 78.0.1 补丁只是简单地将 Requests 从 Setuptools 集成测试中移除
是的,我读到过,而且–除其他事项外–肯定表明这次修改做得不好。在我看来,setuptools 的维护者有时会有一些……奇怪的意见。
但我们还是应该从整体上考虑这个数字。
setuptools 维护者根本没有机会评估他们的改动所带来的影响。整个 Python 生态系统既没有准备也没有装备对其基础进行大规模测试。它极度缺乏 Rust 的环形山,其零散的工具更是如此。
他们要想知道自己会破坏什么,唯一的办法就是推出改动,然后再看。考虑到这一点,令人惊讶的是,这种破坏并不频繁,而且破坏的方式也更糟糕。
>如果能将破坏性更改捆绑在一起就更好了,这样破坏性更改就不会那么频繁出现。
就像……从 2 到 3 的成功过渡?
我个人的经验是: 我可以看出 3.0 和 3.1 是拙劣的,人们还在为 Unicode 字符串前缀争论不休,所以我跳过了这两个版本。从 3.2 版开始,我如沐春风–一大堆我认为是语言设计上的严重问题都得到了简单的解决,一切都*按照它们应该*的方式运行。我再也不用纠结为什么从原始字节数据解码字符串会产生错误,声称编码出了问题,反之亦然。初学者不再在第一天就被教导如何在程序中引入任意代码执行漏洞(input())。新的 print 函数带来了惊人而优雅的效果;“print 1, ”现在与 “print (1,) ”的含义相同,就像你对语言中其他所有功能的期望一样;重定向到文件不再涉及类似 C++ 的神秘语法。在 `int` 类型和独立的 `long` 类型之间不再有速度障碍。诸如此类,不一而足。
然后,我看着周围的人怨声载道、拖拖拉拉,我完全无法理解。
他们得到了额外的时间来解决所有问题,却错过了官方规定的最后期限,之后还不停地抱怨,我还是不明白。
现在也是。
>我还是不明白。
>我还是不明白。
打破世界格局的大变革是不好的,因为它们需要花费大量的时间和金钱来修复。
对我来说,在进行所需的功能开发的同时,进行小规模的弃用移除修改是很容易的。我不需要额外的时间或金钱。它可以在正常的开发过程中完成,没有人会抱怨。
但是,要获得时间和资金来进行一次大的版本升级过渡,从而打破世界格局,那就难上加难了。
对于很多项目来说,这基本上是不可能完成的。
我仍然在维护 Python 2 脚本,它们永远不会被 Python 3 取代。永远不会。
Python 2 的代码甚至还在积极开发中,因为整个项目的生态系统还停留在 Python 2 解释器上。这些项目也不会改变。永远不会。
Python 2 的死亡不会早于仍在使用它的项目。
请不要再犯这样的错误了。
Python 一直被严重破坏,直到 3.5 左右,他们终于重新允许对混合字符串/字节进行 “+”和格式化操作 ( https://peps.python.org/pep-0461/ )。
我花了无数个小时调试由于某个错误处理程序没有对其中一个参数进行正确的编码/解码而导致的问题。真是痛苦不堪。
如果你想让软件具有长期可维护性,你就必须有能力发布向后不兼容的版本,以清理技术债务。
虽然打破应用程序的消费者并不是一件好事,但他们必须为消费记录不兼容的新版本承担一定的责任。
>对新的主要版本的使用承担一定责任
你读过 “报告和更新 ”这篇文章吗?
>如果你想让软件具有长期可维护性、
>你必须有能力发布向后不兼容的版本,以清理技术债务。
这取决于技术债务是什么,消除技术债务的好处是什么,以及有多少下游用户会受到影响。
对于会破坏 12000 个软件包的连字符移除来说,这个比例看起来并不乐观。
> 你读过 “报告和更新 ”这篇文章吗?
我看了,我发现回复中说您已被警告了 3 年多,但却没有采取任何补救措施,所以请不要对您的工作流程被破坏感到惊讶。
我无法控制的依赖链中的一个软件包就会破坏构建。
请始终牢记中断更改的收益和成本之间的权衡。
一个简单的破坏就会破坏 12000 个软件包,这很容易就会浪费人类数年的净工作时间。那么,在接下来的 5 年中为你节省 5 个小时的维护成本是否值得呢?
如果你不需要实际花费/支付工作费用,那么期望每个人都更新并完成工作总是很容易的。
> 如果你不需要实际花费/支付工作费用,那么期望每个人都更新并完成工作总是很容易的。
我们可以很容易地扭转这种局面。从技术上讲,Setuptools 开发人员根本没有义务恢复修改。他们完全可以轻松地说:“‘无担保’和‘重大版本升级’中的哪部分你们不明白?”然后就可以修复所有的错误报告。我们应该感谢他们慷慨地恢复了修改,而不是蔑视当初的修改。
我说的不是是否恢复,而是是否弃用。
如果我们认为 setuptools 的开发者没有义务表现出友好和体贴,那么他们的用户也就没有义务感激他们。你完全可以这样做,但这总是两面性的。
你可以采取 “让别人去处理 ”的态度,但不能强迫用户喜欢你的做法。做不受欢迎的事是变得不受欢迎的可靠途径。
这里有两个与普通库 API 中断不同的地方:
1. 这是一个构建工具。它的预期输入是已发布的软件,顾名思义,这些软件无法根据 API 的变化进行修改。想象一下,如果 GNU Make 决定停止支持一些长期以来被认为不可取的旧 Makefile 功能。这对你使用 make 进行编译的项目来说可能没什么问题,但你很难指望这个新版本的 GNU Make 会被采用,因为它会破坏已发布软件的编译。换句话说,破坏构建工具的 API 会产生很大的影响,因为典型的 API 消费者是被冻结的。
2. 对于 setuptools 来说,不幸的是,所有 Python 旧源代码版本都隐含地使用了最新版本。这使它处于 API 破坏尤其具有破坏性的境地,因为没有选择权。事后看来,从 Python 2.7 或类似版本中提取 distutils 并将其作为隐式默认构建工具可能会更好。
> 关于前端工具无法显示警告的问题,Bravalheri 建议安装程序应该默认向最终用户显示构建警告。
亲爱的 Lazyweb,隐藏构建警告怎么会被认为是一个好主意呢?这让我百思不得其解。
> 亲爱的 Lazyweb,隐藏构建警告怎么会被认为是一个好主意呢?我很困惑。
你真的希望没有线索的人看到无穷无尽的警告吗?和那些不愿意理解的人打交道让我很不爽。难道我真的想和更多不理解的人打交道吗?
别误会我的意思,我不知道如何找到隐藏的警告确实很麻烦。但是,没有线索的人因为他们不懂的无害警告而惊慌失措就更糟糕了。
干杯
沃尔
是的,是的。这里是 UNIX,我们都是成年人。如果有人不明白运行 `rm -rf` 时会发生什么,他很快就会明白的。
不,问题就在这里。Python 和 pip 都是跨平台的,在 Windows 终端用户的机器上触发同样的构建过程也很容易。
> 你真的希望没有线索的人看到无穷无尽的警告吗?
1. 构建东西的人不应该是 “无线索者”,不幸的是,https://lwn.net/Articles/1022219/,但即便如此,运行 “pip install …… ”的人也应该具备一定的计算机知识。
2. 有无数次,在一个团队经验非常丰富的项目中,我是第一个阻止一些旧的构建警告的人。为什么?因为除非构建失败,否则没人会看构建日志。即使失败了,人们也会尽量少看。只看修复构建所需的内容,然后继续。因此,为了那 0.0001% 会查看构建警告的开发人员,千万不要隐藏构建警告。当然,这并不意味着 “在构建日志中加入大量垃圾信息”。但 “警告 ”的定义不应是垃圾。
> 1. 建造东西的人不应该是 “没有线索的人”,不幸的是,https://lwn.net/Articles/1022219/,但即便如此,运行 “pip install …… ”的人也应该有一定的计算机知识。
是你一直在说*用户不是程序员*。
如果有人把从其他地方找到的说明剪切粘贴到电脑上,他们是不可能理解(更不用说修复)这些警告的。他们只是想使用那个软件。
(在$dayjob-1,这些用户大多是玩弄尖端研究工具的博士。而我却不得不把这个 “能在 $desktop 上运行 ”的泥团塞进一个可以通过 CI 环境部署到定制硬件上的软件中。)
> 你应该懂一些计算机知识。
> 是你一直在说*用户不是程序员*。
我知道现在人们越来越难以接受这一点,但一般来说,不可能只把人分成二元对立的两类。使用命令行的人既不可能是 iPhone 用户,也不可能是开发者。中间还有很多其他文化水平的人。你自己就举了一个很好的例子。
> 把从其他地方找到的说明剪贴在一起的人不可能理解(更不用说修复)这些警告。他们只是想使用那个软件。
那又怎样?
如果他们复制/粘贴他们不理解的说明,他们也会忽略他们不理解的警告;事实就是如此!因此,完全没有必要隐藏这些警告(只要它们不占满终端就行)。这对那些不知道自己在做什么的粗心大意的人来说绝对没有任何区别,而对那些关心此事的 0.0001% 的人来说却大不相同。
我知道,关心 0.0001% 的人可能有违直觉。但根据多年的经验,这样做效果很好。
此外,当他们的纸牌屋最终爆炸,向你求助时,你也能看到这些警告。
> 此外,当他们的纸牌屋终于爆炸,他们打电话向你求助时,你可以看到这些警告。
这就是我上一份工作的大部分内容。
不用了,谢谢。
你可能总体上不喜欢这份工作,但如果你首先要说服他们给你发送一些晦涩难懂的日志文件,然后再解释同样的警告,如果直接给他们看,他们本可以更容易地发送给你,那么这份工作肯定不会变得轻松。
这些警告大多会显示给那些甚至不一定*预期会进行构建*的人。Pip 不会提前警告说它将从 sdist 安装,因为它事先并不知道可能必须这样做(以满足依赖版本的限制)。
您可能还对 https://github.com/pypa/pip/issues/9140 感兴趣。
> 这些警告大多会显示给那些甚至不一定*预期会进行构建*的人。
该死。
我认为还是应该显示这些编译警告。事实上,pip 甚至不应该试图掩盖编译与不编译之间的区别;它为什么要这么做?用户界面总是很重要的,但 “pip install ”并不是 “消费者 ”应该使用的东西,它也不是 iPhone,在出现故障时不应该假装一切正常。
此外,要淘汰任何东西都非常困难。对最终用户进行合理的 “垃圾邮件 ”只会有所帮助。
谷歌搜索错误信息和警告是最有用的,每个人都经常这么做。不要阻止这一点。
别再隐藏信息了。忽略日志很简单,每个人都经常这么做。在名称不明的晦涩日志文件或晦涩的 –debug-whatever 选项后面挖掘出有用的警告,是最耗时、最可怕的用户体验。
> 你可能也会对 https://github.com/pypa/pip/issues/9140 感兴趣。
我不感兴趣,因为我写的是 “亲爱的 Lazyweb” 😀
另外,Python 打包看起来很容易让人失去……心理健康点数。我对脚本语言有点失望,但我又知道什么呢(不多)。
好吧,我读了 9140 的描述,我想我同意它。我给它加了票。
> 有人可能会说,Setuptools 已进入第 78 版这一简单事实本身就说明了一个问题–Setuptools 的开发进展太快了。这并不完全公平:大约一半的 Setuptools 主要版本是在基于 pyproject.toml 的新系统实施之前发布的,其中第 39 版是在 2018 年 3 月发布的。不过,我们现在每年仍能看到大约五个新的 Setuptools 主要版本。不用说,将遗留项目更新为现代标准的进展要慢得多。虽然我不希望看到 Setuptools 的开发速度被人为地放慢,但如果能将破坏性的更改捆绑在一起,减少发生的频率,那就更好了。
从 “哇,我刚被一个更新破坏了 ”的角度来看,这在直觉上是很吸引人的,因为你会由此推断出每一次更新都可能会破坏你。但频繁发布的目的恰恰相反:几乎没有更新会让你崩溃。
请记住,这里的问题不是发生了多少破坏。无论版本号是多少,都会以自己的节奏发生。问题在于,你会有怎样的体验。如果在五年的时间里发生了三次对你有影响的破坏性变更,那么直觉上,你会说你希望被破坏一次而不是三次。但实际上,同时应对这三次变革要困难得多。如果你不知道什么地方出了问题,你就有更多的代码需要分割。如果你正试图采用一项新功能,那么在采用该功能之前,你将面临一个轮盘赌,在三分之一的概率下,你将承担三倍的工作量。而且,无论如何,你仍然必须实际处理这三种变化,所以你的工作量并没有减少。
如果你真的非常想要更粗粒度的更新,有多种方法可以实现:你可以在 setuptools 上设置一个全局约束文件,然后很少升级版本;你可以使用某种冻结镜像;你可以使用操作系统打包版本的 setuptools,等等。如果你得到的是所有 78 个版本的 setuptools,那是因为你直接从 PyPI 安装的,而且你可能已经决定,无论 PyPI 上的所有东西是什么版本,都值得你这么做。
例如,Linux 内核已经是 2.6.x 系列的第 74 个版本了。与 setuptools 简洁的整数版本相比,它的实际版本号更奇特一些,但实际上并没有传达任何不同的信息。它在向后兼容性方面的规定严格得出了名。然而,人们并不是每个次版本都升级,而是花大价钱购买十年(或更久)不升级的能力。我想我从未见过这样的提议,即内核 6.14 版的事实意味着内核开发进展太快,最好能减少主线内核版本,每个版本都有更大的改动。
> 请记住,这里的问题不是发生了多少破坏。无论版本号如何,破损都会以自己的速度发生。问题是,你会有怎样的体验。如果在五年的时间里发生了三次对你有影响的破坏性变更,那么直觉上,你会说你希望被破坏一次而不是三次。但实际上,同时应对这三次变革要困难得多。
你忘了,并不是每个人都能按部就班地升级自己部署的一切。特别是考虑到他们所依赖的每个[子[子[子]]]组件都有自己独立的生命周期。
……例如,setuptools 的迭代速度比实际的 python 版本快 5 倍,比大多数部署环境快 10-25 倍[1]。但更有趣的是,我在$dayjob-1 的大部分时间都花在不断清理一些快速迭代的出血边缘依赖、EDA 工具供应商提供的其他依赖[2]以及介于两者之间的所有依赖之间的阻抗错乱。
[1] 即具有 2 年以上发布周期和 ~5 年标准支持生命周期的 LTS/EL 平台
[2] 更准确地说,是由上述 EDA 工具生成的 python 代码。
我同意许多小的破坏性改动往往比大的改动更容易处理(只要修复它们的工作量很小,但并不总是这样)。但我认为,这一切都要假定所做的改动是值得的,也许在某些情况下,大的改动有时会更好,因为它迫使人们重新考虑事情的原貌。
就我所看到的 setuptools 问题而言,由于它的历史悠久,无论 setuptools 做了什么,任何试图使 setuptools 现代化的改动都会破坏大量软件包(而且由于轮子作为发行版的人工制品与构建缓存在很多方面都是最小公分母的选择,人们一直依赖发行版和 setuptools 的遗留功能)。这就意味着,只要软件包的打包配置没有保持更新(即使它可能不需要其他更改),就会出现问题。这也意味着长尾版本会以令人痛苦的方式崩溃。
显而易见的解决方案是对依赖关系树严格把关,并根据需要进行供应商和重写,但如果我们要解决 setuptools 的问题,那么在什么时候分叉它(要么冻结旧版本,要么做一个更稳定的分叉)而不是所有其他软件包才有意义呢?
问题在于,Setuptools 中的这类破坏性改动可能会破坏成千上万的软件包,而影响到的软件包却只占很小的比例。这既是因为 PyPI 有多大,也是因为生态系统的影响(当你的软件包崩溃时,你的依赖包也会跟着崩溃)。如果 Setuptools 将多个变化捆绑在一起,就像 78.0.0 版本中的变化一样,那么任何给定的软件包都不太可能同时处理多个变化。(不过,对于那些最终注定会受到多个此类变更影响的项目,最好还是现在就切换到不同的构建后端)。
另外,如果 Setuptools 的主要版本发布速度较慢,软件包就可以更认真地考虑为用于构建它们的 Setuptools 版本设置版本上限,因为它们可以减少重新评估的频率。
setuptools 最近的另一个问题是 MIT LICENSE 文件被移除 [1],导致它无法打包到发行版中,现在这个问题已经恢复,但在 setuptools 的依赖包中,许可证文件仍然被移除。[2]
[1] https://github.com/pypa/setuptools/issues/5001
[2] https://github.com/jaraco/jaraco.itertools/issues/23
我很同情 setuptools 的维护者。
他们一直在拖着一大堆臭烘烘的排泄物,这些排泄物是二十年来 Python 打包失败的产物,但每当他们试图清理这一大堆乱七八糟的东西时,事情就会来回折腾,因为总有这么一个小的、老的、未维护的包,它的打包脚本晦涩难懂,没有人再能理解它,出于某些晦涩难懂的原因,半个世界似乎都依赖于它,而三十年的废弃期对它来说还不够。
当他们偶尔取得微不足道的成功时,没有人注意,没有人欣赏,也没有人付钱。
但当他们只在百分之二的现有软件包上失败时,整个世界都会义愤填膺地指责他们,而这些人往往连做梦都达不到他们似乎期望的 setuptools 志愿维护者的维护标准。
他们没有任何机会。社区不会帮助他们,他们也没有任何工具来保证 630k 软件包的质量。
我们不会指望一个拿着高薪的木匠只用一把锤子就能建起一座摩天大楼,但我们却似乎指望不拿工资的志愿者在业余时间建造 Python 软件包的七大奇迹。