pip、pipenv是如何处理包于包之间的版本冲突的?

pip、pipenv是如何处理包于包之间的版本冲突的?

三月 11, 2019

最近在开发我们团队的开源项目插件功能,遇到了一个比较棘手的问题。就是插件依赖的版本冲突问题。

1. 插件的定义

我们的项目是基于python的flask框架开发的一个前后端分离的CMS,我们对于插件的定义是这样的:

  • 插件是围绕特定业务场景需求的一系列页面和资源的封装, 如: 日志系统, 推送系统, 电商系统等. 使用插件可快速进行对应业务场景的开发. 你也可以将自己业务中复用的部分开发为插件, 在 Lin CMS 平台与所有开发者进行共享, 或者仅内部进行共享.
  • Lin CMS 插件有三类: A型插件, B型插件, AB型插件. 只涉及前端的插件为 A型插件,只涉及后端的插件为 B型插件,需要前后端对接 API 共同完成一个业务逻辑的插件为 AB型插件
  • 后端插件的定义,插件不同于flask的扩展,扩展是通过pip命令从pypi安装到项目的site_packages目录下面的包。而我们开源项目所谓的插件是基于Lin-CMS的开发规范,为了实现某一特定业务功能或模块的一系列页面或者资源的封装。随着项目的推进,会开发出越来越多适合于各种场景的优秀插件提供给开发者使用。

如果想了解更多Lin-CMS的插件相关知识,请阅读我们的官方文档

2. 为什么会产生版本冲突

为了最大程度上适应不同开发者开发的不同功能、不同依赖的插件,我们Lin CMS主项目在加载这些插件的时候必须比较好的处理这些依赖间的关系,也就是处理版本冲突。

版本冲突主要有两种情况。

  1. 插件与主项目之间的依赖冲突。由于Lin-CMS本身需要安装一些必要的依赖,如flask、flask-JWT这些依赖,那么我们不能保证插件的开发者在插件开发中没有引用这些依赖。版本冲突不可避免。
  2. 插件与插件之间的依赖冲突。由于插件与插件的开发者不同,开发的不同插件也可能引用不同版本的依赖,那么如果依赖间发生冲突,如何取舍,这也是一个问题。

3. 解决版本冲突的方案有哪些?

为了解决依赖的版本冲突问题,我们团队开会讨论得出结构,得到了一下几种解决方案。

  1. 每一个插件发布成pip包。然pip内部处理依赖的冲突问题。
  2. 无论什么时候,都取最高版本的依赖安装。
  3. 发生冲突后不安装,提示错误,用户处理。

方案一如果发布成了pip包,就失去了我们去做插件的本意,pip包的定义以及开发规范与我们插件完全不同,不可取。

方案二,安装最高版本,如果有一些插件的自身或者主项目的依赖版本没有及时更新,我们安装依赖发生冲突的时候,就选择最高版本去安装,新版本的依赖就会覆盖旧版本的依赖,我们无法确定依赖的更新内容,可能会导致之前的依赖不能使用甚至主项目不能使用。插件是挂载在主项目上的,如果主项目发生问题,对于开发者来说,处理起来可能非常棘手。

那么就只剩下方案三了,这种处理方法相对比较合理,因为pip本身也无法处理依赖的冲突问题,采取的是同样的方法。pip官方文档解释如下(pip没有真正的依赖解析机制):

img

4. 解决前的机制的研究

我们最终选取了方案三来解决这个问题。那么问题来了,如何取解决这个问题呢?

4.1 pip安装依赖的机制

首先,我们测试了pip的安装依赖机制。

pip list 查看环境中已安装的依赖

img

选取flask依赖为测试案例,我们的主项目基于 flask 1.0.2,不支持更低的版本。为了测试安装插件依赖的实际场景,开发插件的开发者可能会安装flask的低版本,所以执行 pip install Flask==0.11

img

安装结果如上图,红字显示报错信息:lin-cms 指定了flask==1.0.2,但是你将会安装的flask 0.11 是不兼容的。可是在最后我们发现,flask 0.11还是成功地安装到了我们的环境当中。使用pip list查看版本,已经成功安装。

img

那么pip的机制是:在版本冲突的情况下,虽然会报错,但是已经成功会安装。如果这个时候运行你的项目,会发生不可设想的错误。

我们要做的是阻止pip进行安装,但pip却给成功安装并覆盖了之前的根项目要求的版本。这个是一个待解决的问题

上面我们测试了flask的安装,flask是Lin-CMS一级依赖(top-dependtence),那么在安装Lin-CMS所依赖的子依赖(sub-depentence)时,pip的处理机制是什么呢。(由于pip下无法查看依赖关系,我们使用pipenv graph查看依赖关系。 flask 1.0.2依赖如下

img

我们以click为测试案例,click作为flask依赖的依赖,被要求版本必须大于等于5.1,现在环境下的版本为7.0。我们使用 pip install click==4.1 来安装不符合依赖要求的版本。

img

同样的,安装过程中报错:flask 1.0.2 要求click 大于登录 5.1,但是你将安装的click 4.1 是不兼容的。但是依然安装成功。

现在恢复click的版本为7.0,使用pip install click是行不通的,必须在click后面加上版本号才可以行得通。如下图所示:

img

4.2 pipenv安装依赖的机制

在得出pip安装依赖的机制后,我们使用最几年很火的python包管理工具pipenv来测试pipenv是如何安装依赖的。

和上面的测试pip的部分相同,我们同样在项目的根目录下首先安装flask 0.11。

先查看当前的pipfile

img

执行 pipenv install flask==0.11

img

查看上图中的安装堆栈,画出红线的部分报错:安装失败,其原因是 在分析依赖的时候有版本冲突。再次查看pipfile,flask 0.11 已经被安装到了环境当中。

img

在这里,使用pipenv安装有冲突的依赖和使用pip安装有冲突的依赖报错是一样的。

重复pip做过的第二部,也就是安装sub-dependence click,执行pipenv install click==4.1

img

果然,结果不出所料。pipenv会给我们报错同样的错误,但也会成功把指定依赖的版本安装到系统中。

4.3 对比后的结论

在经过上述一系列的分析后,我们得到结论:pip和pipenv有相同的机制,也就是,当我们向环境中安装有依赖冲突的依赖时,无论是一级依赖(top-dependence)还是子依赖(sub-dependence),pip和pipenv都会告诉我们,你所安装的依赖有不兼容问题,但是他们之后都会成功安装到环境中,需要我们手动恢复到合法的依赖版本。

img

最终解决

好了,机制我们已经了解,处理方法也已经确定,我们现在来实现插件依赖冲突的代码。

在这里,我们可以使用pip的输出结果来判断。也可以使用pipenv的输出结果来判断,但是无论使用什么输出结果作为标准,无法避免得,不合法的版本都会被安装到环境中,这个结果并不是我们想要的。在插件过多的时候,反复安装和卸载包会出现效率的问题,所以,我们决定自己写脚本来判断其依赖关系。

判断依赖关系最好的方法就是使用pipenv graph得到所有的依赖的关系,使用python脚本来分析文本。与插件的依赖进行对比,得到最终要安装的版本,如果不合法,直接停止插件的初始化,抛出一个异常,详细告诉用户某个插件的依赖版本不合法。与跟项目的依赖产生了冲突,省去了先去安装得到错误提示再去卸载这一步过程。在插件的依赖与根目录的依赖是否有冲突的判断通过后(也就是没有冲突),那么分析插件与插件的依赖之间会不会有冲突。给出相应的提示。如果有冲突,只能提示给用户,让用户自己取舍了。