Mozilla的Python3使用情况
Mozilla使用了很多Python。我们的大多数构建系统、CI配置、测试工具、命令行工具和无数其他脚本、工具或Github项目都是由Python处理的。在mozilla-central中有3500多个Python文件(不包括第三方文件),大约包含230万行代码。此外,在Github上的Mozilla org中有462个带有Python标签的存储库(尽管其中许多并不活跃)。这些存储库中有很多Python项目,其中大部分是Python2。
随着Python 2停止支持的日期的临近,这是一个很好的时间来评估当前的情况并提出一些问题。在Python3的迁移中,Mozilla已经走了多远?哪些大型工作项位于关键路径上?我们是否有一个计划能在2020年1月1日Python 2停止支持之前达到一个好的状态?
种树(进行迁移)的第二个最佳的时间
但是在处理这些问题之前,我想先解决另一个经常出现的问题: 随着Python2停止支持,我们是否需要实现100%的迁移?
从技术上讲,不需要。我们仍然可以安装Python 2,并且仍然可以从PyPi上安装包。但继续坚守Python2将是一个巨大的错误,原因如下:
- Python 2将不会再收到安全修复。虽然一个发现的问题对Mozilla的不利影响可能很小,但考虑到Python在Mozilla的所有关键任务中的使用(例如,签名构建),即使是轻微的风险也要认真关注。2018年有三个针对Python 2.7的CVE(漏洞)被发现。再想一下如果我是一个拥有一个Python2漏洞的攻击者,事先知道EOL(停止支持)日期,那么我将把这个漏洞收藏起来直到那个日期之后再进行利用。
- 也许更重要的是,我们所依赖的所有第三方包(有很多)也将停止支持(假设它们还没有放弃支持)。在这些更广泛的包生态系统中,出现漏洞和bug的可能性要大得多,因此很难保证安全性。在2020年后将Python 2及其包生态系统用于关键任务应用程序就是在自找麻烦。
- 延迟意味着需要迁移更多的代码。当你想要与大型Python2代码库进行接口时,你需要编写与Python2兼容的代码。虽然可以编写同时兼容2和3的Python代码,但是这样做的动机并不总是显而易见的。或者缺少技术诀窍。到目前为止,我们在Mozilla上编写的大多数Python代码只兼容Python 2。我们延迟迁移的时间越长,任务就变得越庞大。
- 这是一个持续的机会成本。Python 3于2008年首次发布,在此期间,Python 2中有大量的特性和改进是不可用的。比如异步/等待、异常链接、类型提示和unicode处理。有了Mozilla开发人员单独处理最后一项的时间,我们可能已经完成了整个迁移。
那么,2020年1月1日是一个硬性的最后期限吗?不。但这不会阻止我们以最快的速度进行迁移。不是因为这个最后期限,而是因为这将给Mozilla带来最好的成功机会。
认真考虑迁移到Python3的最佳时机是五年前。第二次最佳的时间是现在。
当前状况
现在,既然我们已经确定了迁移到python3是重要且有价值的,让我们进入细节。本文的其余部分只关注mozilla-central。不是因为我们外部repos中的Python不重要,而是因为我有资格讨论mozilla-central。以下是我们目前取得的一些进展:
- 我们增加了在CI中使用Python3运行python测试的能力。这为我们提供了一个保障,一旦模块的单元测试在Python3下通过,我们就可以相对自信地认为,该模块将来在Python3中依然会运行良好(假设有足够的测试覆盖率)。
- 我们设置了一些linter。一个linter可以确保Python文件至少可以在Python3中导入而不会失败,另一个linter可以确保Python2文件使用合适的的__future__语句,使将来迁移该文件稍微容易一些。尽管这些linter还没有在所有应该启用它的文件上启用。
- 最后,我们开始移植mozbase。它是在我们的构建、测试和CI基础设施中到处使用的一组包。对这些模块进行完全迁移是进行几乎其他所有事情的先决条件。
虽然到目前为止所取得的进展并不明显,但这只是完成全部工作的一小部分。那么接下来会发生什么呢?
下一个主要障碍
最初的重点是添加使用Python 3运行测试的能力,这已经完成了(尽管我们对用于此目的的机制并不完全满意,稍后将详细介绍)。但是,即使我们正在运行测试和linter来捕获潜在的与Python3相关的问题,我们实际上并没有在任何地方默认使用Python3。因此,下一个主要障碍是,用Python 3运行一个简单的mach命令(比如mach google)。从表面上看,这听起来很容易实现,毕竟所有的mach google命令只有四行代码。但实际上,这是一个非常大的项目,我将把本文余下的大部分时间用于这个项目。
使用Python3运行mach命令意味着不仅命令本身需要与Python3兼容,而且所有依赖项也需要兼容。几乎每个命令(包括mach google)都依赖于两个主要库:python/mach和python/mozbuild。让这些模块(或者至少是大多数mach命令使用的那些模块)与Python3一起工作是这里的第一个主要困难。但是,尽管为Python3准备mach和mozbuild是一项繁重的任务,但同时又是一项简单的任务。道路是明确的,我们只需要有人卷起袖子把工作做好。我估计这将不会超过一个星期的工作价值(这只是完成mach google所需的部分的时间)。
另一个阻碍是bootstrapping(引导)。我们验证了开发人员在运行mach bootstrap时安装了受支持的Python版本。我们需要就一个最低可行的版本达成一致(3.5似乎是可能的),然后修改我们的引导脚本,以确保开发人员同时拥有Python 2和Python 3的兼容版本。但这也不是一项非常困难的任务。
这个里程碑的第三个也是最后一个部分是实际实现mach中的管道。要增强对命令进行内省的能力,并确定是否需要使用Python2或Python3运行命令。这就是复杂性所在。
让我们深入研究一下,把需要解决的问题分解一下。这里的基本假设是,这个tree(树)太大了,不能一次全部转换为Python3。代码太多了,bitrot的潜力太大了,在不经意的情况下破坏其他东西的风险太大了。我们必须一次一个来慢慢地转换命令。
调用问题
记住这一点,我们遇到的第一个问题是调用问题。在mach中,命令是通过装饰器注册在实际的Python类上的。如果你以前看过mach_commands.py文件,你可能已经注意到了@CommandProvider、@Command和@CommandArgument装饰器。这为工具作者注册他们的命令和使用的参数提供了一种非常方便的方法。但它也有一个很大的缺点: 每次调用mach时都会导入每一个mach_commands.py。这是mach获得必需的命令元数据来确定它要执行什么的唯一方法。
简而言之,我们使用Python解析所有可用的命令,然后发送用户指定的命令。但是现在我们要发送的命令可能需要一个不同于当前运行的Python。
选项1
如果我们不改变注册的工作方式,这就意味着两件事:
- 每个mach_commands.py和它们在顶层导入的所有东西至少都要在Python 3中可以进行解析(这很可能是很容易实现的)。
- 我们需要为使用了与运行mach_bootstrap迥然不同的Python版本的命令spawn(派生)两个单独的Python解释器。例如,如果我们使用Python3来解析装饰器,那么我们将为需要Python2的命令派生第二个Python进程(反之亦然)。
尽管我们需要实现调用第二个Python进程的实际机制,但这是最简单的解决方案。
选项2
或者,我们可以更改命令注册的工作方式。代替(或者除此之外)使用装饰器,我们可以将发送命令所必须的命令元数据(例如,名称和模块路径)注册在一些主要文件中。也许我们可以使用一个顶层的mach_commands.json文件,它看起来是这样的:
mach二进制文件的内部有一些巧妙的修改,它既是有效的Python,也是有效的shell。当你执行./mach时,它首先会作为shell脚本运行,找到合适的Python可执行文件,然后将自身作为一个Python脚本来重新执行。有了这个提议,mach 驱动程序的shell部分可替换为:
- 解析cli以确定所需的子命令。
- 解析mach_commands.json。
- 根据python键查找Python可执行文件。
- 用合适的Python重新执行自身。
这比选项1复杂得多,但它避免了这两个警告。也就是说,我们不需要担心所有的Python 3都是可导入的,也不需要运行两个单独的Python进程。
通常情况下,我认为这种方法的复杂性远不及这两个微小的好处。但这个选项更有吸引力,因为这是我们一直在讨论要做的事情。这个选项还有第三个更大的好处,尽管它与Python 3迁移完全无关。我们不需要在每次mach调用时都加载每一个mach_commands.py。可以在不导入所有文件的情况下获得发送和运行mach help所需的所有信息。这将大大地加快mach调用。
最终结果是,这两种选项都是可行的。如果我们想把精力集中在Python3迁移上,我会选择选项1。但是选项2仍然很有吸引力,因为它可能会给我们一个同时解决两个实质性问题的理由。在写这篇文章的时候,我不确定我应该选择哪一个选项。
依赖项问题
当你运行并执行mach命令时,系统会创建一个virtualenv,其中包含一组分散在mozilla-central上的“基础" 包。我们称之为“initial(初始)”virtualenv。一些带有更复杂需求的命令确实会在这个基础层上创建它们自己的virtualenv,除此之外,这个“init”virtualenv在默认情况下会被激活。当然,我们在这个virtualenv中安装的包集合是不同的,这取决于我们使用Python2还是Python3来运行命令。我们不能在Python3 virtualenv中安装只适用于Python2的包(反之亦然)。
这里的解决方案非常简单。我们可以维护两个单独的清单来关联这两个必需的virtualenv。一个用于Python2,一个用于Python3。有些模块(甚至大多数模块)可能会同时出现在这两种清单中。但这里还有其他需要考虑的事情。在mozilla-central中,我们需要解决一个类似但无关紧要的问题: 依赖项锁定。
依赖项锁定会确保一个工具的所有使用者使用的版本与其他人完全相同。这使得事情可以保持重现性和明确性,通过验证哈希值可以阻止mitm(中间人攻击)攻击,并且这样做被广泛认为是任何包生态系统中的最佳实践。依赖项锁定值得考虑的原因是,处理依赖项锁定的工具也倾向于处理virtualenv管理。事实上,我们使用了一个这样的工具(Pipenv)来处理当前的依赖项锁定需求。由于无论如何我们都在使用这些类型的工具,因此花一些时间研究它们是否能够帮助我们处理Python 3依赖关系是值得的,那我们就来看一看。
Pipenv
近几年来,Pipenv一直是Python社区的宠儿,我们在很多地方都使用它:
- 当出售第三方软件包时。
- 当运行mach python-test在Python 2 / 3之间切换时(它甚至可以帮助解决调用问题)。
- 对于需要在运行时安装额外的外部包的命令(提供了依赖项锁定)。
当我和Dave Hunt在实现这些事情的时候,这是城里唯一的一款游戏。我们把一切都完成好了,但道路比我们希望的要曲折一些。我们最终实现了一些“足够好”的东西,但是没有达到我们想要的水平。我个人之所以不在Pipenv上出售这些东西以便它们可以在像我们这样的大型monorepo上使用,主要有几个原因:
- 维护者没有协调好外部的变化,我们关闭了几个看起来合理的PR(Pull Request),没有做任何解释(许多潜在的Pipenv贡献者已经注意到了这一趋势)。
- 在我们开发这些系统时,引入了一些bug和向后不兼容的更改。这在Pipenv没有稳定化之前我们使用它时就已经很成问题了,但是版本控制和文档使我们假定存在某种程度的稳定性,而这种稳定性是不存在的。
- 感觉它有点反应迟缓。
- 它包含了许多假设,假设你正在处理一个单个的Python包(而不是用于大型monorepo的工具)。我们必须把它扭曲到它不想去的方向。
我不得不说,我们上一次研究这些系统已经有一年了,我们使用的Pipenv版本也同样老旧。从那时起,情况有可能有所改善。尽管如此,我不建议使用Pipenv来帮助我们。然而…
Poetry
Poetry的创建是为了弥补前面提到的Pipenv的一些缺点。我个人在我自己的几个项目中使用它,并且我认为它是一个非常棒的工具。它看起来更敏捷、更轻量级,至少维护者对提议的新特性是开放讨论的,而且我在使用它时从未遇到过bug或向后不兼容的更改(尽管版本还没有达到1.0,所以向后不兼容的更改还是会遇到的)。
Poetry包含了我在Pipenv中所希望的一切功能,但是它仍然有一个很大的缺点: 它也假设你使用的是单个Python包。它甚至比Pipenv更进一步,迫使你提供诸如包名称和版本之类的元数据。这使得它无法作为大型monorepo的工具后端。那么,我又何必费心提及它呢?
Jetty
Jetty是我一直在构建的一个小实验品。它是一个围绕着Poetry本身的非常微小的包装器,并试图使它在像mozilla-central这样的monorepo中使用时更有用。它做了一些事情:
- 移除定义包元数据的要求。
- 移除包管理命令(例如,包版本冲突)只留下依赖项和virtualenv管理相关命令。
- 提供了用于调用各种命令的编程API(因此我们不必在子进程中运行它)。
它似乎运行得相当好。我的下一步计划是试验用Jetty替换我们的代码树中的 Pipenv用法。如果一切顺利,这可能是处理我们的Python 3依赖关系的一种可行方法。
所有这些关于Pipenv/Poetry/Jetty的讨论,都与当前的问题无关。我们可以在没有它们的情况下解决我们需要的所有问题,这可能是目前最明智的做法。我只是想提到它们,因为它们确实试图解决我们面临的许多相同的问题。它们至少是值得考虑的。
结论及具体步骤
总而言之,下一个主要障碍是开始用Python 3运行特定的mach命令(除了用Python 2运行其他命令之外)。这里有一些具体的步骤可以帮助我们解决这个障碍:
-
用Python 3运行python/mach和python/mozbuild单元测试。
-
尽可能多地启用py3 linter(最好是所有内容)。
-
临时破解mach二进制文件,使其指向Python3,并尝试运行一个非常基本的命令(例如 mach google)。
-
将Python3添加到我们的引导过程中。
与此同时,还有一些更大的问题需要解决。即调用和依赖项问题。在这两种情况下,都有一种快速而又随性的解决方案,以及一种更长但可能更好的解决方案。这两种情况都需要一定程度的规划和协调。
最后,我想回答我在开始时问的一个问题。我们是否计划在2020年1月1日Python 2的EOL之前达到一个好的状态?我的回答是不。这篇文章可能是一个非常粗略的计划大纲,但它只讨论了下一步的主要步骤。在这一步之后,我们仍然有转换所有东西的实际工作要做。另外,本文甚至没有涉及Github中的Python。回答“不”的另一个原因是,尽管一些工程师和团队确实认识到这项工作的重要性,但这并不是高层管理人员关注的事情。我们只是没有必要的资源分配来修复它,我不知道它有没有在其他人的正式计划表上。
话虽如此,我乐观地认为,如果我们按优先顺序来做,这些工作是可以及时完成的。如果我们不这样做,我仍然乐观地认为最终也会完成。只是可能赶不上2020年1月1日。