1import os 2import shutil 3import sys 4import logging 5from distutils.spawn import find_executable 6 7# The `pkg_resources` module is provided by `setuptools`, which is itself a 8# dependency of `virtualenv`. Tolerate its absence so that this module may be 9# evaluated when that module is not available. Because users may not recognize 10# the `pkg_resources` module by name, raise a more descriptive error if it is 11# referenced during execution. 12try: 13 import pkg_resources as _pkg_resources 14 get_pkg_resources = lambda: _pkg_resources 15except ImportError: 16 def get_pkg_resources(): 17 raise ValueError("The Python module `virtualenv` is not installed.") 18 19from tools.wpt.utils import call 20 21logger = logging.getLogger(__name__) 22 23class Virtualenv(object): 24 def __init__(self, path, skip_virtualenv_setup): 25 self.path = path 26 self.skip_virtualenv_setup = skip_virtualenv_setup 27 if not skip_virtualenv_setup: 28 self.virtualenv = find_executable("virtualenv") 29 if not self.virtualenv: 30 raise ValueError("virtualenv must be installed and on the PATH") 31 self._working_set = None 32 33 @property 34 def exists(self): 35 # We need to check also for lib_path because different python versions 36 # create different library paths. 37 return os.path.isdir(self.path) and os.path.isdir(self.lib_path) 38 39 @property 40 def broken_link(self): 41 python_link = os.path.join(self.path, ".Python") 42 return os.path.lexists(python_link) and not os.path.exists(python_link) 43 44 def create(self): 45 if os.path.exists(self.path): 46 shutil.rmtree(self.path) 47 self._working_set = None 48 call(self.virtualenv, self.path, "-p", sys.executable) 49 50 @property 51 def bin_path(self): 52 if sys.platform in ("win32", "cygwin"): 53 return os.path.join(self.path, "Scripts") 54 return os.path.join(self.path, "bin") 55 56 @property 57 def pip_path(self): 58 path = find_executable("pip3", self.bin_path) 59 if path is None: 60 raise ValueError("pip3 not found") 61 return path 62 63 @property 64 def lib_path(self): 65 base = self.path 66 67 # this block is literally taken from virtualenv 16.4.3 68 IS_PYPY = hasattr(sys, "pypy_version_info") 69 IS_JYTHON = sys.platform.startswith("java") 70 if IS_JYTHON: 71 site_packages = os.path.join(base, "Lib", "site-packages") 72 elif IS_PYPY: 73 site_packages = os.path.join(base, "site-packages") 74 else: 75 IS_WIN = sys.platform == "win32" 76 if IS_WIN: 77 site_packages = os.path.join(base, "Lib", "site-packages") 78 else: 79 site_packages = os.path.join(base, "lib", "python{}".format(sys.version[:3]), "site-packages") 80 81 return site_packages 82 83 @property 84 def working_set(self): 85 if not self.exists: 86 raise ValueError("trying to read working_set when venv doesn't exist") 87 88 if self._working_set is None: 89 self._working_set = get_pkg_resources().WorkingSet((self.lib_path,)) 90 91 return self._working_set 92 93 def activate(self): 94 if sys.platform == 'darwin': 95 # The default Python on macOS sets a __PYVENV_LAUNCHER__ environment 96 # variable which affects invocation of python (e.g. via pip) in a 97 # virtualenv. Unset it if present to avoid this. More background: 98 # https://github.com/web-platform-tests/wpt/issues/27377 99 # https://github.com/python/cpython/pull/9516 100 os.environ.pop('__PYVENV_LAUNCHER__', None) 101 path = os.path.join(self.bin_path, "activate_this.py") 102 with open(path) as f: 103 exec(f.read(), {"__file__": path}) 104 105 def start(self): 106 if not self.exists or self.broken_link: 107 self.create() 108 self.activate() 109 110 def install(self, *requirements): 111 try: 112 self.working_set.require(*requirements) 113 except Exception: 114 pass 115 else: 116 return 117 118 # `--prefer-binary` guards against race conditions when installation 119 # occurs while packages are in the process of being published. 120 call(self.pip_path, "install", "--prefer-binary", *requirements) 121 122 def install_requirements(self, requirements_path): 123 with open(requirements_path) as f: 124 try: 125 self.working_set.require(f.read()) 126 except Exception: 127 pass 128 else: 129 return 130 131 # `--prefer-binary` guards against race conditions when installation 132 # occurs while packages are in the process of being published. 133 call( 134 self.pip_path, "install", "--prefer-binary", "-r", requirements_path 135 ) 136