Photo by Cam Morin on Unsplash
本文介绍了在 Python 库中 vendor 第三方库的正确方法。我知道这篇文章的受众非常狭窄,大部分 Python 开发者都不会也不需要用到这个技术, 但是本着分享的精神还是把它总结一二,作为软件的作者更是应该尊重所有其他库的作者的劳动。
Vendor,直译供应商,在软件中(比如 C, Go 等语言中),是一种把第三方库的代码直接内嵌到软件中的方式。 它不同于通过依赖文件指定的方式,第三方库的代码是直接包含在软件中的,有可能原样保留也有可能经过修改,所以需要注意各种 License 的限制, 特别是如果上游库采用了 GPL 系列的协议,使用 vendor 的软件也是要受到传染的。
正如我开头说的,适用范围非常狭窄,有三种场景:
pip
。pip._vendor
中包含了 25 个依赖。pip
是现行标准的 Python 安装器,所以它不能 有任何依赖,否则为了装 pip
,要先装这些依赖,而这些依赖又只能通过 pip
安装,这就递归了。除此之外,还包括像 setuptools
这样的基础构建工具。third-party-lib==1.0.0
, 会导致与之共存的同样依赖此库的软件无法解析版本,造成依赖冲突。而如果转成 vendor,就相当于把这个非常严格的依赖限制去掉了。其实,针对上述的第 2、3 种场景,也不是非 vendor 不可。除了 vendor,还可以 fork 到自己的 git 仓库,再使用 git 依赖 引入,或者发布为一个新的 PyPI 包。只是 vendor 是一个最轻松的方式。
还有一个限制条件:对 Python 来说,只有纯 Python 的库才能 vendor。
vendor 并不是简单地复制粘贴这种传统艺能就解决了的,在我看来,它还要注意以下两点:
所以,vendor 并不是复制粘贴,只是在开源框架下对现状的一种妥协,我们最终的目标,是消灭 vendor。
在 Python 中,除了把 vendor 库都放到代码库下一个目录中(比如 mypackage/vendor
)以外,还需要修改所有的 import 语句,指向到这个目录中。 比如,把 import requests
改成 from mypackage.vendor import requests
。PDM 中也包含了这样一个目录,我是使用和 pip
相同的工具来管理 vendor 的。 这个工具是 vendoring,文档很少(因为就没人要用)。它包含以下几个功能:
requirements.txt
下载依赖到指定目录使用过程,也大致按上面的步骤。首先建立一个 mypackage/vendor
目录,在其中创建一个 vendors.txt
,填写依赖(requirements.txt
格式):
requests==2.24.1
click==8.0.1
然后在项目根路径下的 pyproject.toml
中,添加以下内容:
[tool.vendoring]
destination = "mypackage/vendor/" # vendor目录路径
requirements = "mypackage/vendor/vendors.txt" # requirements路径
namespace = "mypackage.vendor" # import 重命名前缀
protected-files = ["__init__.py", "README.md", "vendors.txt"] # 每次重新 vendor 时需要保留的文件
patches-dir = "tasks/patches" # patch 文件目录
[tool.vendoring.transformations]
substitute = [ # 重命名没有覆盖到的 import,文件替换规则
{match = '__import__("requests")', replace = '__import__("mypackage.vendor.requests")'}
]
drop = [ # 需要从 vendor 库中去除的文件
"bin/",
"*.so",
"typing.*",
"*/tests/"
]
最后运行 vendoring sync
,就会自动把 vendor 全部准备好了。
对于 patch 文件,其实就是 git diff
的输出,有了这个文件,git 就可以从源代码重新建立 vendor 目录。生成方法:
vendoring sync
,把文件提交到本地仓库(只 commit 不 push)git diff --patch <file_path> > <patches_dir>/<file_name>.patch
,把 patch 文件保存到 patches_dir
中from mypackage.vendor import requests
改成 import requests
1git add . && git commit --amend
,提交修改vendoring sync
验证一下,如果一切正常,应该不会产生任何变更,说明这个 vendor 过程是 reproducible 的