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