360案例讲解:如何使用virtualenv?

jopen 10年前

virtualenv 是一个创建隔离的Python环境的工具。 virtualenv要解决的根本问题是库的版本和依赖,以及权限问题。假设你有一个程序,需要LibFoo的版本1,而另一个程序需要版本2,如何同时使用两个应用程序呢?如果将所有的库都安装在 /usr/lib/python2.7/site-packages(或者你的系统的标准包安装路径),非常容易出现将不该升级的库升级的问题。另外,在一台共享的机器上,如果没有全局的 site-packages 目录的权限(例如一个共享的主机),如何安装Python库呢?在这些情况下,就是该用到virtualenv的地方。它能够创建一个自己的安装目录,形成一个独立的环境,不会影响其他的virtualenv环境,甚至可以不受全局的site-packages当中安装的包的影响。

virtualenv默认只能生成一个“干净”的Python环境,其中只有python及其标准库。而360需要有一个脚本,可以一键做到:

  1. 在开发环境当中,生成基本环境之后,自动安装基本的开发库,如 PyLint, nose, coverage等,并将源代码目录当中的所有项目注册到 site-packages(执行 python setup.py develop)
  2. 在线上环境当中,环境生成完毕之后,自动安装应用程序及其依赖库。

幸好virtualenv支持生成一个定制化的脚本,可以用来生成自己的虚拟环境,关键是 virtualenv.create_bootstrap_script 方法,这个方法支持在生成的定制化脚本的末尾添加一段自己的代码,而且支持在默认的环境创建完成之后调用自定义的 after_install 方法。360写了自己的一个 after_install 方法,加载同一目录下的 after_install.py 文件并执行其中的 main 函数。

在这个main函数当中,将after_install的需求整理成下面几种类型的动作:

  • 用 easy_install 安装指定的包,支持指定 pypi 服务器地址。
  • 将未打包的一些脚本或二进制文件复制到新的 python 环境当中,比如一些私有C库的 python 绑定。
  • 枚举当前目录下的代码目录,并对其中的 setup.py 执行 develop 操作

第一个和第二个问题都容易解决。建立一个配置文件,在其中指定 pypi 服务器的地址,easy_install 需要安装的包列表,需要复制到 python 环境当中的文件的文件名称,并将这些文件放在与 after_install.py 同一目录的 data 子目录下,而after_install.py只要忠实地按照配置文件当中的配置进行 easy_install 或复制的操作即可。

比较复杂的是第三点,因为当前目录当中可能有多个项目,而这些项目之间是具有相互依赖的,同时他们可能都会依赖于一些外部的python包。如果不谨慎地考虑 develop 的顺序,可能导致 develop 一个较为高层的库的时候,将它依赖的底层库从 pypi 上面获取到并安装到环境当中,而后续 develop 这个底层库的时候,有可能造成两个库的冲突。

对于这种情况,需要先枚举出当前目录下的所有包含有 setup.py 文件的目录,然后获取到其中的依赖关系,并根据依赖关系进行排序,最后根据这个顺序依次进行 develop。

获取一个项目(python包)的依赖关系,只能从 setup.py 文件当中抽取。关于 setup.py 当中定义包和包的依赖关系的内容,请参见 distribute 的文档。 360使用 monkey patch 的方法,hook了 setup 函数,在枚举出所有项目的 setup.py 文件之后,动态加载每个 setup.py(使用 imp.load_source),也就获取到了相应的依赖关系。下面的代码当中,_get_project_depends 接受一个 project 名称,和一个 project 所在目录的名字,即可获取这个项目所依赖的 Python 包的列表。

import setuptools  def _get_project_depends(project, project_path):      if not getattr(setuptools, '_is_hook_', False):          setuptools._is_hook_ = True          _hook_setuptools()      setup_py = os.path.join(project_path, 'setup.py')      if not os.path.exists(setup_py):          return None      setuptools._this_round_depends = []      imp.load_source('%s_setup' % project, setup_py)      return setuptools._this_round_depends     def _hook_setuptools():      def _hook_find_packages(*args, **kwargs):          return []      setuptools.find_packages = _hook_find_packages      def _hook_setup(name=None, install_requires=[], dependency_links=[], **kwargs):          for depend in install_requires:              setuptools._this_round_depends.append(depend.replace('-', '_'))          for depend in dependency_links:              setuptools._this_round_depends.append(depend)      setuptools.setup = _hook_setup
后根据每个项目的依赖关系,做成一个字典,key就是Python包的名字,而value是该包所依赖的包的列表。根据依赖关系进行排序这个问题,也常常被拿来做面试题,有兴趣的同仁也可以自己实现一下:
def sort_with_depends(dep_map):      """根据 dep_map 对其中的 key进行排序。      :param dict dep_map: 依赖图关系,key是每个项目,value是该项目依赖的项目(一个list)      :return: 一个排好序的列表,其中每个项目所依赖的项目都在它前面出现      """


至此,after_install.py 扩展脚本的功能就不是问题了。这样,有了一个通过 virtualenv.create_bootstrap_script 生成的bootstrap.py脚本,一个 after_install.py 作为扩展脚本,一个配置文件用来配置 after_install,以及一个目录用来保存所有需要直接复制的文件。他们的入口是 bootstrap.py,直接用 python 执行它即可创建一个 virtaulenv 环境,然后自动调用 after_install.py 当中的逻辑。一切看起来很完美,但是需要的可能更多:需要一个独立的可执行的脚本,以便大家方便的创建起来一个新环境,而不是每次复制一堆文件。

为此360使用了类似 eggsecutable 的方式。这种方式依赖于Linux unzip程序的一个特性。unzip程序在试图解压一个zip文件的时候,并不要求一定要从第一个字节开始就是zip文件,它会在一定的区域内搜索 zip 文件的头,并从对应的位置开始解压。这样一来,我们可以在一个zip文件的之前,附加上一个简短的 shell 脚本,在其中将自身解压,并将后续的控制权交给解压出来的文件。比如360所做出的一个附加了zip文件的shell脚本:

#! /bin/sh  PYTHON_WITH_VERSION=python2.7  if [ -z $1 ]; then      echo "Require environment name."      exit 1  fi  if [ -e .bootstrap ]; then      rm -rf .bootstrap  fi  unzip -q -d.bootstrap.tmp $0 >/dev/null 2>&1  mv .bootstrap.tmp .bootstrap  $PYTHON_WITH_VERSION .bootstrap/bootstrap.py --no-site-packages $*  exit 0

这之后就是一个zip文件,通过unzip可以方便地将其解压。明白了原理,就可以很容易地写出一个 bootstrap.sh 文件的生成器 make-bootstrap。借助这个生成器,可以方便地生成一个自包含的 bootstrap.sh文件。对应于需要的场景,有两种方式使用这个脚本:

方式一:预先生成好一个 bootstrap.sh,并将这个文件签入到svn当中,放在开发环境的根目录下;每个开发人员签出代码之后,立即执行这个脚本构建一个自己的开发环境:./bootstrap.sh dev-sharelib

后续开始开发之前,可以运行:source dev-sharelib/bin/activate

激活这个开发环境,随后即可在其中进行开发和测试了。

方式二:在应用程序的构建过程当中,调用 make-bootstrap 脚本生成一个指定配置的 bootstrap.sh(为此360还写了一个 distribute扩展),并将其发布到更新服务器。在部署阶段,运维人员获取这个bootstrap.sh并在服务器上建立起来一个隔离环境用来部署整个应用。

至此,就有了一套基于virtualenv的环境建立脚本及其生成方案,基本的环境隔离和低权限化已经完成。但真正的开发工作,以及发布和部署,都还需要另一个python大杀器 – distribute 才行。

原文地址:http://blogs.360.cn/blog/how-360-uses-python-1-virtualenv/