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