是否应该采用 Python 3 一直是 Python 社区争论的焦点话题。虽然 Python 3 如今获得了普遍的支持,一些很是受欢迎的项目(如 Django)已经彻底放弃了 Python 2,但这个争论在必定程度上仍然存在。对于咱们来讲,有一些关键因素影响着咱们的决定:python
使人兴奋的新特性编程
Python 3 带来了快速的创新。除了一长串通常性改进以外,一些特别的特性引发了咱们的注意:bootstrap
类型注解语法:咱们的代码库很是大,所以使用类型注解对于提高开发人员的工做效率来讲是很是重要的。咱们是 MyPy 的忠实粉丝,所以原生支持类型注解天然会吸引到咱们。架构
协程函数语法:咱们严重依赖线程模型和消息传递来开发咱们的不少功能。asyncio 项目及其 async/await 语法有时能够消除对回调的需求,从而让代码变得更清晰。app
老旧的工具链async
随着 Python 2 变得老旧,相应的工具链在很大程度上也已通过时了。所以,继续使用 Python 2 将伴随着日益增长的维护负担:编程语言
使用旧编译器 / 运行时限制了咱们升级某些重要依赖项的能力。例如,咱们在 Windows 和 Linux 上使用 Qt:因为包含了 Chromium(经过 QtWebEngine),最新版本的 Qt 须要更现代的编译器。ide
随着继续深刻与操做系统集成,咱们没法依赖这些工具链的更新版本,所以增长了采用新 API 的成本。例如,Python 2 仍然须要 Visual Studio 2008,而微软再也不支持这个版本,而且与 Windows 10 SDK 不兼容。模块化
freezer 和脚本函数
最初,咱们依靠“freezer”脚本为各个平台建立原生应用程序。可是,咱们不是直接使用原生工具链(例如在 macOS 上使用 Xcode),而是将平台二进制文件的建立委托给了第三方工具,好比用于 Windows 的 py2exe,用于 macOS 的 py2app 和用于 Linux 的 bbfreeze。这个基于 Python 的构建系统受到 distutils 的启发:咱们的应用程序最初只是一个 Python 包,因此咱们使用了一个相似 setup.py 的构建脚本。
随着时间的推移,咱们的代码库变得愈来愈异构化。如今,Python 再也不是咱们使用的惟一的编程语言。咱们如今的代码包含了 TypeScript/HTML、Rust 和 Python,以及用于某些特定平台的 Objective-C 和 C++。为了支持全部这些组件,setup.py 脚本(内部叫做 build-all.py)变得庞大而混乱,难以维护。
转折点来自于咱们与每一个操做系统集成的转变:首先,咱们开始逐步引入愈来愈先进的操做系统扩展(如 Smart Sync 内核组件),这些扩展一般不是使用 Python 编写的。其次,微软和苹果开始强制使用更复杂的新工具(一般是专有工具,例如代码签名)来部署应用程序。
例如,macOS 10.10 引入了一个新的应用程序扩展 FinderSync,用于与 Finder 集成。FinderSync 扩展不只仅是一个 API,它仍是一个完整的应用程序包(.appex),它具备自定义生命周期规则(由操做系统启动)以及对进程间通讯更严格的要求。经过 Xcode 能够很容易地利用这些扩展,但 py2app 并不彻底支持它们。
所以,咱们面临两个问题:
Python 2 对使用新的工具链形成阻碍,提高了使用新 API 的成本(例如在 Windows 10 上使用 Windows 运行时)。
freezer 脚本也提高了部署原生代码的成本(例如在 macOS 上构建应用程序扩展)。
要迁移到 Python 3,咱们须要作出选择:修改 freezer 依赖项,增长对 Python 3(以及现代编译器)和平台特定特性(如 app 扩展)的支持,或者抛弃以 Python 为中心的构建系统,完全废除“freezer”。咱们选择了后者。
至于 pyinstaller,咱们在项目早期考虑过使用它,但它当时不支持 Python 3,更重要的是,它与 freezer 同样也存在相似的限制。它是个好东西,只是不符合咱们的要求。
嵌入 Python
为了解决这个构建和部署问题,咱们决定将 Python 运行时嵌入到原生应用程序中。咱们没有将这个过程委托给 freezer,而是使用每一个平台特定的工具(例如 Windows 上的 Visual Studio)来构建各类入口点。此外,咱们将 Python 代码代码抽离成一个库,以便更直接地支持与其余语言的“混合和匹配”。
这样咱们就能够直接使用每一个平台的 IDE 和工具链(例如在 macOS 上添加 FinderSync),同时仍然可使用 Python 编写应用程序逻辑。
咱们采用了如下结构:
原生入口点:这些入口点与每一个平台的应用程序模型兼容,包括应用程序扩展,例如 Windows 上的 COM 组件或 macOS 上的 app 扩展。
使用多种语言(包括 Python)编写共享库。
从表面上看,应用程序将更加相似于平台所指望的,但在各类库背后,咱们的团队能够更灵活地使用他们喜欢的编程语言或工具。
这种架构所带来的模块化能力还产生了一个关键的反作用:如今能够同时部署 Python 2 和 Python 3 库。在 Python 3 迁移中使用这种方法须要两个步骤:第一,围绕 Python 2 实现新架构,第二,使用 Python 3 代替 Python 2。
第 1 步:“Anti-freeze”
咱们的第一步是中止使用 freezer 脚本。bbfreeze 和 pywin32 都缺少对 Python 3 的支持,因此咱们别无选择。从 2016 年开始,咱们开始逐步作出这一改变。
首先,咱们将配置 Python 运行时和启动 Python 线程的工做抽离到一个叫做 libdropbox_bootstrap 的库中。这个库能够完成以前由 freezer 脚本完成的一些工做。虽然咱们在很大程度上已经再也不须要依赖这些脚本,但仍然须要为运行 Python 代码的提供一些基本的东西:
打包代码,以便在设备上运行
这须要确保咱们提供的是通过编译的 Python“字节码”,而不是 Python 源代码。以前,每一个 freezer 脚本都有本身的存储格式,咱们借这个机会引入了一种单一的格式,用于在全部平台上打包咱们的代码:
对于 Python 字节码.pyc,单个 ZIP 压缩包(例如 python-packages-35.zip)包含了全部必需的 Python 模块。
对于原生扩展.pyd/.so,它们是平台原生 DLL,被安装在一个特定位置,以确保应用程序能够加载到它们。例如,在 Windows 上,它们与入口点(即 Dropbox.exe)放在一块儿。
使用 modulegraph 来打包。
隔离 Python 解释器
这样能够防止应用程序执行设备上的其余 Python 代码。有趣的是,Python 3 让这种类型的嵌入变得更加容易。例如,借助新的 Py_SetPath 函数,咱们能够很容易地隔离代码,避免了在 Python 2 中隔离 freezer 脚本须要作的那些比较复杂的工做。为了可以在 Python 2 中进行隔离,咱们将这个函数反向移植到自定义的分支代码库中。
其次,咱们引入了特定于各个平台的入口点,如 Dropbox.exe、Dropbox.app 和 dropboxd,并让这些入口点使用这个库。这些入口点是使用每一个平台的“标准”工具构建的:Visual Studio、Xcode 和 make,这样咱们就能够删除 freezer 脚本中的大部分自定义拼凑代码。例如,在 Windows 上,这极大地简化了为 Dropbox.exe 配置 DEP/NX 以及嵌入应用程序清单和包含资源文件。
关于 Windows 的说明:在这个时间点上,继续使用 Visual Studio 2008 的成本变得很是高。咱们须要一个可以同时支持 Python 2 和 Python 3 的版本,因而咱们选择了 Visual Studio 2013。为了支持它,咱们对 Python 2 的自定义分支进行了大量修改,以便可以正常编译。为这些变化所作出的努力进一步加强了咱们的信念:转向 Python 3 是正确的决定。
第 2 步:Hydra
进行这么大规模的转换(咱们的应用程序包含超过 100 万个 Python LOC,并被安装超过数亿次)是一个渐进的过程:咱们不能简单粗暴地在一个版本中搞定一切。固然,这个与咱们的发布流程也有关系,咱们每两周向全部用户发布一个新版本。咱们须要找到一种方法,让少许用户先用上 Python 3,便于及早发现和修复 bug。
为实现这一目标,咱们决定同时使用 Python 2 和 Python 3 来构建 Dropbox。这要求:
可以同时提供 Python 2 和 Python 3“软件包”,以及字节码和扩展。
在转换期间强制混合使用 Python 2 和 Python 3 语法。
咱们利用了前一个步骤引入的嵌入式设计:将 Python 抽离为单独的库,能够很容易地引入另外一个版本的变体。而后,在入口点(例如 Dropbox.app)初始化期间,能够选择要使用的 Python 版本。
这是经过手动将入口点连接到 libdropbox_bootstrap 来实现的。例如,在 macOS 和 Linux 上,在选定了 Python 版本以后,咱们就使用 dlopen/dlsym。在 Windows 上,咱们使用 LoadLibrary 和 GetProcAddress。
在加载 Python 以前须要选择运行时 Python 解释器,在开发时使用命令行参数 /py3,在实际部署时使用磁盘文件来设置,这样就能够经过咱们的功能门控系统 Stormcrow 来控制它。
所以,咱们在启动 Dropbox 客户端时可以动态地选择 Python 版本。咱们也所以可以在 CI 基础设施上设置额外的做业来运行针对 Python 3 的单元测试和集成测试。咱们还对代码提交队列进行自动检查,防止某些代码变动出现回退。
在经过自动化测试得到了足够的信心以后,咱们开始向真实用户推出基于 Python 3 的版本。咱们经过远程功能门控系统来逐步向用户推出新客户端。咱们先是将新版本推给 Dropboxer,这样就能找出并修复大多数潜在的问题。而后,咱们向一小部分用户推出 Beta 版本,并最终扩展到了稳定版渠道:在 7 个月内,全部 Dropbox 用户都开始使用 Python 3 版本。为了最大限度地提升质量,咱们制定了一个策略,在向更大的用户群推出新版本以前,必须修复全部与迁移相关的问题。
直到版本 52,咱们才完成了整个迁移过程:Python 2 已经从 Dropbox 的桌面客户端中完全移除。