Published on

Debian 系统上捉摸不定的 Python

Authors

在上周的周记中我记了一句:

  • pdm 提了几个 issue,都和 debian 系统的 python 有关,i hate it

本文是对这句话的一个扩展。作为一个 Python 打包工具的开发者,非常痛恨 Debian 系统,所以我在回复 laixintao 时说道: Python 打包系统的混乱,Debian 系统是要居大功的。其实不止 Debian 是如此,下面开始吐槽,非常不严谨,吐槽完我就跑。

Python 中的 Install Scheme

Install Scheme 的概念,中文大概是翻译成「安装蓝图」?意思就是提供了一个路径的集合,告诉 Python 包的安装器(如 pip),什么文件应该放到哪个路径下。 本文只打算讨论 purelibplatlib,也就是 Python 库应该放到哪里。Install Scheme 可以通过以下函数获得:

import sysconfig
install_paths = sysconfig.get_paths()
print(install_paths['purelib'])

正常情况下,比如我之前的文章提到的一样,Python 库是放到:

  • Posix: $path_prefix/lib/pythonX.Y/site-packages
  • Windows: $path_prefix/Lib/site-packages

$path_prefix 是 Python 解释器所在的路径前缀。在这里我们先请优秀学生 Windows 回到座位上,来说说 Posix 的问题,比如 Python 路径是 /usr/bin/python3.9,那么 /usr 是路径前缀,上述路径则变为 /usr/lib/python3.9/site-packages

Debian:事实并非如此

基本所有的 Linux 发行版都会自己打包 Python 库,他们不信任 PyPI,所有用到的 Python 库都拿源码下来,自己打包。那问题来了,那么多 Python 包,那么多 Python 版本,怎么打,怎么安装?他们发现了一点:纯 Python 库是比较兼容的,不需要每个 Python 版本都打一个包,只需要整个 Python 3 打一个包就可以了,所以有 python3-pip, python3-requests 这种命名方式的包。安装的时候,也把它们放在一个地方,所有 Python 3 版本都可以用。其他不是纯 Python 的包,再分版本存放。(这段是我臆想,不严谨)

所以,在 Debian 上,就有了下面三个路径存放 Python 库:

  1. /usr/lib/python3/dist-packagesapt 安装的纯 Python 库
  2. /usr/lib/python3.9/dist-packages/apt 安装的带扩展的 Python 库
  3. /usr/local/lib/python3.9/dist-packages/pip3 安装的 Python 库

这属于自己的设计了,那么就需要改安装库和导入库的逻辑。Debian 维护了一系列的补丁 来干这件事,改完之后,sys.path 会包含上面三个路径,site-packages 的路径从中去除了,而 pip3 也会安装包到第三个路径。所以要记住,发行版上自带的 Python 和 pip 都是特制的,你用从官网和 PyPI 上下载的去替换是会出问题的。

OK,这没问题,也能接受,反正你自己魔改自己发布到 apt,只要闭环了用户是感知不到变化的。但问题是,这些补丁是不完备的!。为了改默认的 sys.path,Debian 只修改了 site 模块,以及 distutils1 模块,而没有修改上面的 sysconfig 模块,而 distutils 模块已经在 PEP 632 里被 Deprecated 了,官方推荐的替代就是 sysconfig。这个问题随着 Python 3.10 的发布而暴露了出来,Debian 的维护者也意识到这个问题,最新的补丁里已经修改了 sysconfig,但不是很小心,造成了一些回归问题。这样的后果就是,如果用户切换发行版的不同版本,以及 Python 3.10 或 3.10 以下,这个 Install Scheme 安装到哪里完全捉摸不定,不可预测,那么包管理器就很难做了(还能怎样,当然是原谅(兼容)啊,难道威胁用户,不许用 apt 的版本吗?)

到底有多捉摸不定?

做了一个测试,分别测试 Python 3.9 和 3.10,以及 pip 使用 apt 的版本和 get-pip.py 安装的版本,在旧版本 Debian2debian:testing 中获取 Install Scheme 的值。

ImagePython 3.9Python 3.10
ubuntu:focalsysconfig: 未修改
site: 已修改
distutils: 已修改
get-pip.py 安装的 pip 把库安装到 site-packages
sysconfig: 已修改3
site: 已修改
distutils: api 已移除
get-pip.py 安装 pip 失败
debian:testingsysconfig: 未修改
site: 已修改
distutils: 已修改
get-pip.py 安装的 pip 把库安装到 site-packages
sysconfig: 已修改
site: 已修改
distutils: 已修改
get-pip.py 安装的 pip 把库安装到 dist-packages

这里已修改是指返回 dist-packages 的路径,而未修改是指返回 site-packages 的路径。测试脚本见仓库。

可以看到在 Python < 3.10 上虽然补丁不完备,行为倒还是统一的,但在 Python 3.10 上就出幺蛾子了,简直就是一团乱麻,Python 环境太难了。

Bonus: MacOS 上的 Python,取决于它是不是 framework,安装路径也有区别,但这已经在 CPython 的标准库中得到支持,不是用补丁方式解决的。 所以大家对下面这个主张没有异议了吧:

Windows 上的 Python 环境是最清爽干净的。

如何避坑?

两条建议:

  1. 如果要在容器中使用 Python,用 python 系列的镜像。那里面的 Python 是标准化的,不是特制的。
  2. 如果用户系统是 Debian 系,不要在系统路径中安装包,对,--user 都不行,甚至 python3-pip 都不要装以绝后患。而应该使用虚拟环境(python3-venv 包)和 pipx。或者用 PDM 或 Poetry 这种包管理工具。因为只有在虚拟环境中,Python 库的安装路径永远是 site-packages,无论在哪个系统上。

一些 GitHub issues

Footnotes

  1. distutils.sysconfig 中也包含一些和 sysconfig 模块中作用类似的函数。

  2. 因为 deadsnakes ppa 只支持 ubuntu,我在这里用的其实是 ubuntu:focal 镜像。

  3. 在 focal 上没有 Python 3.10 的系统包,所以这里是从 deadsnakes ppa 安装的。

Share: