如何在CI/CD管道中管理本地Python的依赖性?

365 阅读8分钟

应用程序的依赖性就像一个城市的排水系统:当适当的建立和良好的管理,它们在我们建立的应用程序中变得不可见,而我们会忘记它们。

建立一个Python项目可以像使用 pip 在我们开始之前安装所有我们需要的依赖项一样简单 。这可能是可行的--如果我们是唯一在这个项目上工作的人,而且我们只打算在我们的开发机器上运行它--但它不能扩展。扩展需要一个自动化的CI/CD管道,以使安装这些依赖的工作可以重复进行。

我们可以通过在requirements.txt文件中列出我们项目的依赖关系来消除混乱,使它们易于安装。理想情况下,这可以让我们执行一条命令,使我们的应用程序准备好运行--作为一个额外的好处,这使这个过程可以重复。当我们把requirements.txt添加到源码控制中时,我们可以很容易地用它来使我们的Python应用程序在其他开发者的机器上、在CI/CD管道中、以及在生产中运行起来。

这种方法甚至可能适用于某些库,如TensorFlowNumPy,它们包含大量的本地C、C++或Fortran代码。然而,这种方法只有在库在PyPI上为我们的平台提供了预建的二进制轮子时才能很好地工作--而且我们愿意相信别人的二进制。

当然,我们不能总是指望为我们的目标平台提供预建的轮子。此外,我们雇主的安全政策可能要求我们从头开始构建依赖关系,以确保我们没有使用被破坏的本地依赖关系。编写我们自己的依赖关系是确保我们的 软件供应链安全的一个好方法。

从头开始构建本地依赖关系有其挑战。我们中的许多人都是Python开发人员,几乎没有设置和使用本地构建工具链的背景。不过,通过足够的试验和错误,我们通常可以使它们在我们的开发机器上工作。

但是,如果我们需要在CI/CD管道中为多个平台建立我们的应用程序的本地依赖关系,该怎么办? 这是一个很常见的要求,但要弄清楚从哪里开始可能会很困难。幸运的是,有几种方法可以完成这项工作。我们可以手动安装和更新我们的依赖(这需要时间和精力),或者使用现代软件包管理器来建立一个自动化和可重复的CI/CD管道。

在多个平台的CI/CD管道中管理本地Python依赖项可能非常耗时

对于任何需要运送给终端用户的应用程序,建立CI/CD管道是一个好主意。不管我们要运送的是哪种类型的应用程序,CI/CD流水线都会给我们提供一种自动化的、可重复的方式来构建、测试、打包或部署我们的应用程序。

如果我们的应用只有纯粹的Python依赖,而且我们只需要支持一个目标平台,那么建立一个CI/CD管道就相对容易。

但是,如果我们必须构建我们的原生依赖并支持多个平台(例如,Linux、Windows和macOS),事情就变得复杂了。在这种情况下,我们将需要:

  • 设置一个虚拟机,为每个目标平台运行一个构建环境。
  • 将每个构建环境与我们的CI/CD平台集成,通常是通过安装一个代理,让CI/CD平台在我们的构建机器上调用命令。
  • 在每个构建环境中安装一个完整的构建工具链。这通常因操作系统的不同而不同。例如,如果我们要构建一个包含C++代码的依赖,我们需要在Linux构建环境中安装GCC,在Windows构建环境中安装MSVC编译器,在macOS上安装Clang。
  • 添加我们的本地依赖项所需要的任何构建自动化工具,如CMake、Meson或Bazel。

我们还必须处理安装本地依赖关系所需的任何头文件和库文件的问题。毕竟,如果我们想从源码构建我们的本地依赖关系,我们可能也想从源码构建我们的依赖关系。这样做的方法各不相同。在我们的Linux构建环境中,我们可能会使用内置的操作系统包管理器来安装库的源代码包。如果我们在Windows上工作,我们很可能要手动解压我们的库源。如果我们在macOS上开发,如果可以的话,我们会使用Homebrew这样的软件包管理器。但是,如果Homebrew仓库缺乏我们需要的库,我们就必须手动下载并解压它们。

这是一个很大的工作量--即使一切都很顺利。作为Python开发者,我们大多数人都不是C或C++的构建专家。让我们回想一下,你上次试图从头开始构建一个本地 Python 依赖关系的时候。它在第一次尝试时就完美地工作了吗?这通常需要一些试验和错误。

如果我们需要构建一个像NumPy这样的库,它同时包含C和Fortran代码,那就变得更加复杂了。如果设置三个C/C++的构建环境很有挑战性,那么请系好安全带:我们还需要在所有的构建环境中安装一个Fortran构建工具链和本地依赖源。

我们也可以决定保留本地依赖性源代码的本地副本,以确保它没有被篡改。保留依赖源的私人副本是一种良好的安全做法。然而,它增加了依赖性混淆的风险--也就是说,我们误拉了依赖性的远程副本而不是使用我们的私有副本。

为我们的CI/CD管道设置三个本地构建环境,听起来是一个很大的工作。但是,如果有足够的时间,这是我们可以处理的事情。

不幸的是,这个过程通常还有很多问题。如果我们需要在所有三个操作系统上支持32位和64位的构建,我们现在需要设置总共六个构建环境。如果我们需要为Windows 7、Windows 10、Ubuntu和CentOS分别进行构建呢?那就有更多的构建环境需要设置了。

但我们还没有完成。我们的应用程序的原生依赖并没有被冻结在时间中。他们将获得新的功能和错误修复,我们不想忽视。我们已经看到 过时的依赖关系会造成多大的损害 。因此,我们的本地依赖将不断地更新他们的依赖,这意味着我们必须通过每一个构建环境,更新我们所安装的支持库和头文件。

这个过程开始听起来像一个全职工作。我们的第一直觉可能是只建立容器化的构建。这是一个好主意,因为这是一个创建可重复使用的构建环境的好方法,我们可以在任何需要的地方启动--无论是在开发人员的笔记本电脑上还是在云端作为我们CI/CD管道的一部分。

然而,容器化构建可能为我们节省的工作比我们预期的少。我们仍然需要忍受一轮又一轮的试验和错误,这些试验和错误是我们在试图建立可运行的构建虚拟机时经历的。我们还需要为每个构建环境维护单独的Docker文件。

如果我们需要支持macOS,我们的容器计划将无法实施。没有苹果认可的方法可以在容器中运行macOS。我们最好的选择是在苹果硬件上的macOS主机中旋转运行macOS虚拟机。

在所有这些之后,我们必须把我们建立的依赖关系从每个构建机器上拿下来,这样我们就可以在测试和打包我们的应用程序时,在CI/CD管道中进一步使用它们。

哇,光是阅读 这个过程就已经很累了。作为有经验的Python开发者,如果有必要的话,我们也许可以实现它。但是我们想这样做吗?我们中的大多数人都想把时间花在写Python上,而不是看管本地构建环境。