1""" 2Virtual environment (venv) package for Python. Based on PEP 405. 3 4Copyright (C) 2011-2014 Vinay Sajip. 5Licensed to the PSF under a contributor agreement. 6""" 7import logging 8import os 9import shutil 10import subprocess 11import sys 12import types 13 14logger = logging.getLogger(__name__) 15 16 17class EnvBuilder: 18 """ 19 This class exists to allow virtual environment creation to be 20 customized. The constructor parameters determine the builder's 21 behaviour when called upon to create a virtual environment. 22 23 By default, the builder makes the system (global) site-packages dir 24 *un*available to the created environment. 25 26 If invoked using the Python -m option, the default is to use copying 27 on Windows platforms but symlinks elsewhere. If instantiated some 28 other way, the default is to *not* use symlinks. 29 30 :param system_site_packages: If True, the system (global) site-packages 31 dir is available to created environments. 32 :param clear: If True, delete the contents of the environment directory if 33 it already exists, before environment creation. 34 :param symlinks: If True, attempt to symlink rather than copy files into 35 virtual environment. 36 :param upgrade: If True, upgrade an existing virtual environment. 37 :param with_pip: If True, ensure pip is installed in the virtual 38 environment 39 :param prompt: Alternative terminal prefix for the environment. 40 """ 41 42 def __init__(self, system_site_packages=False, clear=False, 43 symlinks=False, upgrade=False, with_pip=False, prompt=None): 44 self.system_site_packages = system_site_packages 45 self.clear = clear 46 self.symlinks = symlinks 47 self.upgrade = upgrade 48 self.with_pip = with_pip 49 self.prompt = prompt 50 51 def create(self, env_dir): 52 """ 53 Create a virtual environment in a directory. 54 55 :param env_dir: The target directory to create an environment in. 56 57 """ 58 env_dir = os.path.abspath(env_dir) 59 context = self.ensure_directories(env_dir) 60 # See issue 24875. We need system_site_packages to be False 61 # until after pip is installed. 62 true_system_site_packages = self.system_site_packages 63 self.system_site_packages = False 64 self.create_configuration(context) 65 self.setup_python(context) 66 if self.with_pip: 67 self._setup_pip(context) 68 if not self.upgrade: 69 self.setup_scripts(context) 70 self.post_setup(context) 71 if true_system_site_packages: 72 # We had set it to False before, now 73 # restore it and rewrite the configuration 74 self.system_site_packages = True 75 self.create_configuration(context) 76 77 def clear_directory(self, path): 78 for fn in os.listdir(path): 79 fn = os.path.join(path, fn) 80 if os.path.islink(fn) or os.path.isfile(fn): 81 os.remove(fn) 82 elif os.path.isdir(fn): 83 shutil.rmtree(fn) 84 85 def ensure_directories(self, env_dir): 86 """ 87 Create the directories for the environment. 88 89 Returns a context object which holds paths in the environment, 90 for use by subsequent logic. 91 """ 92 93 def create_if_needed(d): 94 if not os.path.exists(d): 95 os.makedirs(d) 96 elif os.path.islink(d) or os.path.isfile(d): 97 raise ValueError('Unable to create directory %r' % d) 98 99 if os.path.exists(env_dir) and self.clear: 100 self.clear_directory(env_dir) 101 context = types.SimpleNamespace() 102 context.env_dir = env_dir 103 context.env_name = os.path.split(env_dir)[1] 104 prompt = self.prompt if self.prompt is not None else context.env_name 105 context.prompt = '(%s) ' % prompt 106 create_if_needed(env_dir) 107 env = os.environ 108 if sys.platform == 'darwin' and '__PYVENV_LAUNCHER__' in env: 109 executable = os.environ['__PYVENV_LAUNCHER__'] 110 else: 111 executable = sys.executable 112 dirname, exename = os.path.split(os.path.abspath(executable)) 113 context.executable = executable 114 context.python_dir = dirname 115 context.python_exe = exename 116 if sys.platform == 'win32': 117 binname = 'Scripts' 118 incpath = 'Include' 119 libpath = os.path.join(env_dir, 'Lib', 'site-packages') 120 else: 121 binname = 'bin' 122 incpath = 'include' 123 libpath = os.path.join(env_dir, 'lib', 124 'python%d.%d' % sys.version_info[:2], 125 'site-packages') 126 context.inc_path = path = os.path.join(env_dir, incpath) 127 create_if_needed(path) 128 create_if_needed(libpath) 129 # Issue 21197: create lib64 as a symlink to lib on 64-bit non-OS X POSIX 130 if ((sys.maxsize > 2**32) and (os.name == 'posix') and 131 (sys.platform != 'darwin')): 132 link_path = os.path.join(env_dir, 'lib64') 133 if not os.path.exists(link_path): # Issue #21643 134 os.symlink('lib', link_path) 135 context.bin_path = binpath = os.path.join(env_dir, binname) 136 context.bin_name = binname 137 context.env_exe = os.path.join(binpath, exename) 138 create_if_needed(binpath) 139 return context 140 141 def create_configuration(self, context): 142 """ 143 Create a configuration file indicating where the environment's Python 144 was copied from, and whether the system site-packages should be made 145 available in the environment. 146 147 :param context: The information for the environment creation request 148 being processed. 149 """ 150 context.cfg_path = path = os.path.join(context.env_dir, 'pyvenv.cfg') 151 with open(path, 'w', encoding='utf-8') as f: 152 f.write('home = %s\n' % context.python_dir) 153 if self.system_site_packages: 154 incl = 'true' 155 else: 156 incl = 'false' 157 f.write('include-system-site-packages = %s\n' % incl) 158 f.write('version = %d.%d.%d\n' % sys.version_info[:3]) 159 160 if os.name == 'nt': 161 def include_binary(self, f): 162 if f.endswith(('.pyd', '.dll')): 163 result = True 164 else: 165 result = f.startswith('python') and f.endswith('.exe') 166 return result 167 168 def symlink_or_copy(self, src, dst, relative_symlinks_ok=False): 169 """ 170 Try symlinking a file, and if that fails, fall back to copying. 171 """ 172 force_copy = not self.symlinks 173 if not force_copy: 174 try: 175 if not os.path.islink(dst): # can't link to itself! 176 if relative_symlinks_ok: 177 assert os.path.dirname(src) == os.path.dirname(dst) 178 os.symlink(os.path.basename(src), dst) 179 else: 180 os.symlink(src, dst) 181 except Exception: # may need to use a more specific exception 182 logger.warning('Unable to symlink %r to %r', src, dst) 183 force_copy = True 184 if force_copy: 185 shutil.copyfile(src, dst) 186 187 def setup_python(self, context): 188 """ 189 Set up a Python executable in the environment. 190 191 :param context: The information for the environment creation request 192 being processed. 193 """ 194 binpath = context.bin_path 195 path = context.env_exe 196 copier = self.symlink_or_copy 197 copier(context.executable, path) 198 dirname = context.python_dir 199 if os.name != 'nt': 200 if not os.path.islink(path): 201 os.chmod(path, 0o755) 202 for suffix in ('python', 'python3'): 203 path = os.path.join(binpath, suffix) 204 if not os.path.exists(path): 205 # Issue 18807: make copies if 206 # symlinks are not wanted 207 copier(context.env_exe, path, relative_symlinks_ok=True) 208 if not os.path.islink(path): 209 os.chmod(path, 0o755) 210 else: 211 subdir = 'DLLs' 212 include = self.include_binary 213 files = [f for f in os.listdir(dirname) if include(f)] 214 for f in files: 215 src = os.path.join(dirname, f) 216 dst = os.path.join(binpath, f) 217 if dst != context.env_exe: # already done, above 218 copier(src, dst) 219 dirname = os.path.join(dirname, subdir) 220 if os.path.isdir(dirname): 221 files = [f for f in os.listdir(dirname) if include(f)] 222 for f in files: 223 src = os.path.join(dirname, f) 224 dst = os.path.join(binpath, f) 225 copier(src, dst) 226 # copy init.tcl over 227 for root, dirs, files in os.walk(context.python_dir): 228 if 'init.tcl' in files: 229 tcldir = os.path.basename(root) 230 tcldir = os.path.join(context.env_dir, 'Lib', tcldir) 231 if not os.path.exists(tcldir): 232 os.makedirs(tcldir) 233 src = os.path.join(root, 'init.tcl') 234 dst = os.path.join(tcldir, 'init.tcl') 235 shutil.copyfile(src, dst) 236 break 237 238 def _setup_pip(self, context): 239 """Installs or upgrades pip in a virtual environment""" 240 # We run ensurepip in isolated mode to avoid side effects from 241 # environment vars, the current directory and anything else 242 # intended for the global Python environment 243 cmd = [context.env_exe, '-Im', 'ensurepip', '--upgrade', 244 '--default-pip'] 245 subprocess.check_output(cmd, stderr=subprocess.STDOUT) 246 247 def setup_scripts(self, context): 248 """ 249 Set up scripts into the created environment from a directory. 250 251 This method installs the default scripts into the environment 252 being created. You can prevent the default installation by overriding 253 this method if you really need to, or if you need to specify 254 a different location for the scripts to install. By default, the 255 'scripts' directory in the venv package is used as the source of 256 scripts to install. 257 """ 258 path = os.path.abspath(os.path.dirname(__file__)) 259 path = os.path.join(path, 'scripts') 260 self.install_scripts(context, path) 261 262 def post_setup(self, context): 263 """ 264 Hook for post-setup modification of the venv. Subclasses may install 265 additional packages or scripts here, add activation shell scripts, etc. 266 267 :param context: The information for the environment creation request 268 being processed. 269 """ 270 pass 271 272 def replace_variables(self, text, context): 273 """ 274 Replace variable placeholders in script text with context-specific 275 variables. 276 277 Return the text passed in , but with variables replaced. 278 279 :param text: The text in which to replace placeholder variables. 280 :param context: The information for the environment creation request 281 being processed. 282 """ 283 text = text.replace('__VENV_DIR__', context.env_dir) 284 text = text.replace('__VENV_NAME__', context.env_name) 285 text = text.replace('__VENV_PROMPT__', context.prompt) 286 text = text.replace('__VENV_BIN_NAME__', context.bin_name) 287 text = text.replace('__VENV_PYTHON__', context.env_exe) 288 return text 289 290 def install_scripts(self, context, path): 291 """ 292 Install scripts into the created environment from a directory. 293 294 :param context: The information for the environment creation request 295 being processed. 296 :param path: Absolute pathname of a directory containing script. 297 Scripts in the 'common' subdirectory of this directory, 298 and those in the directory named for the platform 299 being run on, are installed in the created environment. 300 Placeholder variables are replaced with environment- 301 specific values. 302 """ 303 binpath = context.bin_path 304 plen = len(path) 305 for root, dirs, files in os.walk(path): 306 if root == path: # at top-level, remove irrelevant dirs 307 for d in dirs[:]: 308 if d not in ('common', os.name): 309 dirs.remove(d) 310 continue # ignore files in top level 311 for f in files: 312 srcfile = os.path.join(root, f) 313 suffix = root[plen:].split(os.sep)[2:] 314 if not suffix: 315 dstdir = binpath 316 else: 317 dstdir = os.path.join(binpath, *suffix) 318 if not os.path.exists(dstdir): 319 os.makedirs(dstdir) 320 dstfile = os.path.join(dstdir, f) 321 with open(srcfile, 'rb') as f: 322 data = f.read() 323 if not srcfile.endswith('.exe'): 324 try: 325 data = data.decode('utf-8') 326 data = self.replace_variables(data, context) 327 data = data.encode('utf-8') 328 except UnicodeError as e: 329 data = None 330 logger.warning('unable to copy script %r, ' 331 'may be binary: %s', srcfile, e) 332 if data is not None: 333 with open(dstfile, 'wb') as f: 334 f.write(data) 335 shutil.copymode(srcfile, dstfile) 336 337 338def create(env_dir, system_site_packages=False, clear=False, 339 symlinks=False, with_pip=False, prompt=None): 340 """Create a virtual environment in a directory.""" 341 builder = EnvBuilder(system_site_packages=system_site_packages, 342 clear=clear, symlinks=symlinks, with_pip=with_pip, 343 prompt=prompt) 344 builder.create(env_dir) 345 346def main(args=None): 347 compatible = True 348 if sys.version_info < (3, 3): 349 compatible = False 350 elif not hasattr(sys, 'base_prefix'): 351 compatible = False 352 if not compatible: 353 raise ValueError('This script is only for use with Python >= 3.3') 354 else: 355 import argparse 356 357 parser = argparse.ArgumentParser(prog=__name__, 358 description='Creates virtual Python ' 359 'environments in one or ' 360 'more target ' 361 'directories.', 362 epilog='Once an environment has been ' 363 'created, you may wish to ' 364 'activate it, e.g. by ' 365 'sourcing an activate script ' 366 'in its bin directory.') 367 parser.add_argument('dirs', metavar='ENV_DIR', nargs='+', 368 help='A directory to create the environment in.') 369 parser.add_argument('--system-site-packages', default=False, 370 action='store_true', dest='system_site', 371 help='Give the virtual environment access to the ' 372 'system site-packages dir.') 373 if os.name == 'nt': 374 use_symlinks = False 375 else: 376 use_symlinks = True 377 group = parser.add_mutually_exclusive_group() 378 group.add_argument('--symlinks', default=use_symlinks, 379 action='store_true', dest='symlinks', 380 help='Try to use symlinks rather than copies, ' 381 'when symlinks are not the default for ' 382 'the platform.') 383 group.add_argument('--copies', default=not use_symlinks, 384 action='store_false', dest='symlinks', 385 help='Try to use copies rather than symlinks, ' 386 'even when symlinks are the default for ' 387 'the platform.') 388 parser.add_argument('--clear', default=False, action='store_true', 389 dest='clear', help='Delete the contents of the ' 390 'environment directory if it ' 391 'already exists, before ' 392 'environment creation.') 393 parser.add_argument('--upgrade', default=False, action='store_true', 394 dest='upgrade', help='Upgrade the environment ' 395 'directory to use this version ' 396 'of Python, assuming Python ' 397 'has been upgraded in-place.') 398 parser.add_argument('--without-pip', dest='with_pip', 399 default=True, action='store_false', 400 help='Skips installing or upgrading pip in the ' 401 'virtual environment (pip is bootstrapped ' 402 'by default)') 403 parser.add_argument('--prompt', 404 help='Provides an alternative prompt prefix for ' 405 'this environment.') 406 options = parser.parse_args(args) 407 if options.upgrade and options.clear: 408 raise ValueError('you cannot supply --upgrade and --clear together.') 409 builder = EnvBuilder(system_site_packages=options.system_site, 410 clear=options.clear, 411 symlinks=options.symlinks, 412 upgrade=options.upgrade, 413 with_pip=options.with_pip, 414 prompt=options.prompt) 415 for d in options.dirs: 416 builder.create(d) 417 418if __name__ == '__main__': 419 rc = 1 420 try: 421 main() 422 rc = 0 423 except Exception as e: 424 print('Error: %s' % e, file=sys.stderr) 425 sys.exit(rc) 426